@@ -70,6 +70,20 @@ EMAIL_FROM_ADDRESS=from_address@gmail.com |
||
70 | 70 |
# Number of lines of log messages to keep per Agent |
71 | 71 |
AGENT_LOG_LENGTH=200 |
72 | 72 |
|
73 |
+######################################################################################################## |
|
74 |
+# OAuth Configuration # |
|
75 |
+# More information at the wiki: https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications # |
|
76 |
+######################################################################################################## |
|
77 |
+ |
|
78 |
+TWITTER_OAUTH_KEY= |
|
79 |
+TWITTER_OAUTH_SECRET= |
|
80 |
+ |
|
81 |
+THIRTY_SEVEN_SIGNALS_OAUTH_KEY= |
|
82 |
+THIRTY_SEVEN_SIGNALS_OAUTH_SECRET= |
|
83 |
+ |
|
84 |
+GITHUB_OAUTH_KEY= |
|
85 |
+GITHUB_OAUTH_SECRET= |
|
86 |
+ |
|
73 | 87 |
############################# |
74 | 88 |
# AWS and Mechanical Turk # |
75 | 89 |
############################# |
@@ -8,7 +8,7 @@ rvm: |
||
8 | 8 |
- 2.1.1 |
9 | 9 |
- 1.9.3 |
10 | 10 |
before_install: |
11 |
- - travis_retry gem install bundler |
|
11 |
+ - travis_retry gem install bundler |
|
12 | 12 |
before_script: |
13 | 13 |
- mysql -e 'create database huginn_test;' |
14 | 14 |
- bundle exec rake db:migrate db:test:prepare |
@@ -56,6 +56,8 @@ gem 'uglifier', '>= 1.3.0' |
||
56 | 56 |
gem 'select2-rails', '~> 3.5.4' |
57 | 57 |
gem 'jquery-rails', '~> 3.1.0' |
58 | 58 |
gem 'ace-rails-ap', '~> 2.0.1' |
59 |
+gem 'spectrum-rails' |
|
60 |
+ |
|
59 | 61 |
|
60 | 62 |
# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5 |
61 | 63 |
# in its own Gemfile. |
@@ -86,6 +88,11 @@ gem 'slack-notifier', '~> 0.5.0' |
||
86 | 88 |
gem 'therubyracer', '~> 0.12.1' |
87 | 89 |
gem 'mqtt' |
88 | 90 |
|
91 |
+gem 'omniauth' |
|
92 |
+gem 'omniauth-twitter' |
|
93 |
+gem 'omniauth-37signals' |
|
94 |
+gem 'omniauth-github' |
|
95 |
+ |
|
89 | 96 |
group :development do |
90 | 97 |
gem 'binding_of_caller' |
91 | 98 |
gem 'better_errors' |
@@ -191,12 +191,33 @@ GEM |
||
191 | 191 |
net-ftp-list (3.2.8) |
192 | 192 |
nokogiri (1.6.3.1) |
193 | 193 |
mini_portile (= 0.6.0) |
194 |
+ oauth (0.4.7) |
|
194 | 195 |
oauth2 (0.9.4) |
195 | 196 |
faraday (>= 0.8, < 0.10) |
196 | 197 |
jwt (~> 1.0) |
197 | 198 |
multi_json (~> 1.3) |
198 | 199 |
multi_xml (~> 0.5) |
199 | 200 |
rack (~> 1.2) |
201 |
+ omniauth (1.2.2) |
|
202 |
+ hashie (>= 1.2, < 4) |
|
203 |
+ rack (~> 1.0) |
|
204 |
+ omniauth-37signals (1.0.5) |
|
205 |
+ omniauth (~> 1.0) |
|
206 |
+ omniauth-oauth2 (~> 1.0) |
|
207 |
+ omniauth-github (1.1.2) |
|
208 |
+ omniauth (~> 1.0) |
|
209 |
+ omniauth-oauth2 (~> 1.1) |
|
210 |
+ omniauth-oauth (1.0.1) |
|
211 |
+ oauth |
|
212 |
+ omniauth (~> 1.0) |
|
213 |
+ omniauth-oauth2 (1.1.2) |
|
214 |
+ faraday (>= 0.8, < 0.10) |
|
215 |
+ multi_json (~> 1.3) |
|
216 |
+ oauth2 (~> 0.9.3) |
|
217 |
+ omniauth (~> 1.2) |
|
218 |
+ omniauth-twitter (1.0.1) |
|
219 |
+ multi_json (~> 1.3) |
|
220 |
+ omniauth-oauth (~> 1.0) |
|
200 | 221 |
orm_adapter (0.5.0) |
201 | 222 |
pg (0.17.1) |
202 | 223 |
polyglot (0.3.5) |
@@ -293,6 +314,8 @@ GEM |
||
293 | 314 |
simplecov-html (0.8.0) |
294 | 315 |
slack-notifier (0.5.0) |
295 | 316 |
slop (3.6.0) |
317 |
+ spectrum-rails (1.3.4) |
|
318 |
+ railties (>= 3.1) |
|
296 | 319 |
sprockets (2.11.0) |
297 | 320 |
hike (~> 1.2) |
298 | 321 |
multi_json (~> 1.0) |
@@ -400,6 +423,10 @@ DEPENDENCIES |
||
400 | 423 |
mysql2 (~> 0.3.16) |
401 | 424 |
net-ftp-list (~> 3.2.8) |
402 | 425 |
nokogiri (~> 1.6.1) |
426 |
+ omniauth |
|
427 |
+ omniauth-37signals |
|
428 |
+ omniauth-github |
|
429 |
+ omniauth-twitter |
|
403 | 430 |
pg |
404 | 431 |
protected_attributes (~> 1.0.8) |
405 | 432 |
pry |
@@ -418,6 +445,7 @@ DEPENDENCIES |
||
418 | 445 |
select2-rails (~> 3.5.4) |
419 | 446 |
shoulda-matchers |
420 | 447 |
slack-notifier (~> 0.5.0) |
448 |
+ spectrum-rails |
|
421 | 449 |
therubyracer (~> 0.12.1) |
422 | 450 |
twilio-ruby (~> 3.11.5) |
423 | 451 |
twitter (~> 5.8.0) |
@@ -6,6 +6,7 @@ |
||
6 | 6 |
#= require json2 |
7 | 7 |
#= require jquery.json-editor |
8 | 8 |
#= require latlon_and_geo |
9 |
+#= require spectrum |
|
9 | 10 |
#= require ./worker-checker |
10 | 11 |
#= require_self |
11 | 12 |
|
@@ -182,6 +183,8 @@ $(document).ready -> |
||
182 | 183 |
|
183 | 184 |
$(".description").html(json.description_html) if json.description_html? |
184 | 185 |
|
186 |
+ $('.oauthable-form').html(json.form) if json.form? |
|
187 |
+ |
|
185 | 188 |
if $("#agent_options").hasClass("showing-default") || $("#agent_options").val().match(/\A\s*(\{\s*\}|)\s*\Z/g) |
186 | 189 |
window.jsonEditor.json = json.options |
187 | 190 |
window.jsonEditor.rebuild() |
@@ -12,6 +12,7 @@ |
||
12 | 12 |
*= require select2-bootstrap |
13 | 13 |
*= require jquery.json-editor |
14 | 14 |
*= require rickshaw |
15 |
+ *= require spectrum |
|
15 | 16 |
*= require_tree . |
16 | 17 |
*= require_self |
17 | 18 |
*/ |
@@ -186,3 +187,17 @@ h2 .scenario, a span.label.scenario { |
||
186 | 187 |
.color-success { |
187 | 188 |
color: #5cb85c; |
188 | 189 |
} |
190 |
+ |
|
191 |
+.form-group { |
|
192 |
+ .sp-replacer { |
|
193 |
+ @extend .form-control; |
|
194 |
+ } |
|
195 |
+ |
|
196 |
+ .sp-preview { |
|
197 |
+ width: 100%; |
|
198 |
+ } |
|
199 |
+ |
|
200 |
+ .sp-dd { |
|
201 |
+ display: none; |
|
202 |
+ } |
|
203 |
+} |
@@ -1,9 +1,50 @@ |
||
1 | 1 |
module LiquidDroppable |
2 | 2 |
extend ActiveSupport::Concern |
3 | 3 |
|
4 |
+ # In subclasses of this base class, "locals" take precedence over |
|
5 |
+ # methods. |
|
4 | 6 |
class Drop < Liquid::Drop |
5 |
- def initialize(object) |
|
7 |
+ class << self |
|
8 |
+ def inherited(subclass) |
|
9 |
+ class << subclass |
|
10 |
+ attr_reader :drop_methods |
|
11 |
+ |
|
12 |
+ # Make all public methods private so that #before_method |
|
13 |
+ # catches everything. |
|
14 |
+ def drop_methods! |
|
15 |
+ return if @drop_methods |
|
16 |
+ |
|
17 |
+ @drop_methods = Set.new |
|
18 |
+ |
|
19 |
+ (public_instance_methods - Drop.public_instance_methods).each { |name| |
|
20 |
+ @drop_methods << name.to_s |
|
21 |
+ private name |
|
22 |
+ } |
|
23 |
+ end |
|
24 |
+ end |
|
25 |
+ end |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ def initialize(object, locals = nil) |
|
29 |
+ self.class.drop_methods! |
|
30 |
+ |
|
6 | 31 |
@object = object |
32 |
+ @locals = locals || {} |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ def before_method(name) |
|
36 |
+ if @locals.include?(name) |
|
37 |
+ @locals[name] |
|
38 |
+ elsif self.class.drop_methods.include?(name) |
|
39 |
+ __send__(name) |
|
40 |
+ end |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ def each |
|
44 |
+ return to_enum(__method__) unless block_given? |
|
45 |
+ self.class.drop_methods.each { |name| |
|
46 |
+ yield [name, __send__(name)] |
|
47 |
+ } |
|
7 | 48 |
end |
8 | 49 |
end |
9 | 50 |
|
@@ -1,6 +1,23 @@ |
||
1 | 1 |
module LiquidInterpolatable |
2 | 2 |
extend ActiveSupport::Concern |
3 | 3 |
|
4 |
+ included do |
|
5 |
+ validate :validate_interpolation |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ def valid?(context = nil) |
|
9 |
+ super |
|
10 |
+ rescue Liquid::Error |
|
11 |
+ errors.empty? |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ def validate_interpolation |
|
15 |
+ interpolated |
|
16 |
+ rescue Liquid::Error => e |
|
17 |
+ errors.add(:options, "has an error with Liquid templating: #{e.message}") |
|
18 |
+ false |
|
19 |
+ end |
|
20 |
+ |
|
4 | 21 |
def interpolate_options(options, event = {}) |
5 | 22 |
case options |
6 | 23 |
when String |
@@ -0,0 +1,32 @@ |
||
1 |
+module Oauthable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |base| |
|
5 |
+ @valid_oauth_providers = :all |
|
6 |
+ attr_accessible :service_id |
|
7 |
+ validates_presence_of :service_id |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ def oauthable? |
|
11 |
+ true |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ def valid_services_for(user) |
|
15 |
+ if valid_oauth_providers == :all |
|
16 |
+ user.available_services |
|
17 |
+ else |
|
18 |
+ user.available_services.where(provider: valid_oauth_providers) |
|
19 |
+ end |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ def valid_oauth_providers |
|
23 |
+ self.class.valid_oauth_providers |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ module ClassMethods |
|
27 |
+ def valid_oauth_providers(*providers) |
|
28 |
+ return @valid_oauth_providers if providers == [] |
|
29 |
+ @valid_oauth_providers = providers |
|
30 |
+ end |
|
31 |
+ end |
|
32 |
+end |
@@ -1,8 +1,10 @@ |
||
1 | 1 |
module TwitterConcern |
2 | 2 |
extend ActiveSupport::Concern |
3 |
+ include Oauthable |
|
3 | 4 |
|
4 | 5 |
included do |
5 | 6 |
validate :validate_twitter_options |
7 |
+ valid_oauth_providers :twitter |
|
6 | 8 |
end |
7 | 9 |
|
8 | 10 |
def validate_twitter_options |
@@ -15,19 +17,19 @@ module TwitterConcern |
||
15 | 17 |
end |
16 | 18 |
|
17 | 19 |
def twitter_consumer_key |
18 |
- options['consumer_key'].presence || credential('twitter_consumer_key') |
|
20 |
+ ENV['TWITTER_OAUTH_KEY'] |
|
19 | 21 |
end |
20 | 22 |
|
21 | 23 |
def twitter_consumer_secret |
22 |
- options['consumer_secret'].presence || credential('twitter_consumer_secret') |
|
24 |
+ ENV['TWITTER_OAUTH_SECRET'] |
|
23 | 25 |
end |
24 | 26 |
|
25 | 27 |
def twitter_oauth_token |
26 |
- options['oauth_token'].presence || options['access_key'].presence || credential('twitter_oauth_token') |
|
28 |
+ service.token |
|
27 | 29 |
end |
28 | 30 |
|
29 | 31 |
def twitter_oauth_token_secret |
30 |
- options['oauth_token_secret'].presence || options['access_secret'].presence || credential('twitter_oauth_token_secret') |
|
32 |
+ service.secret |
|
31 | 33 |
end |
32 | 34 |
|
33 | 35 |
def twitter |
@@ -31,14 +31,15 @@ class AgentsController < ApplicationController |
||
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
def type_details |
34 |
- agent = Agent.build_for_type(params[:type], current_user, {}) |
|
34 |
+ @agent = Agent.build_for_type(params[:type], current_user, {}) |
|
35 | 35 |
render :json => { |
36 |
- :can_be_scheduled => agent.can_be_scheduled?, |
|
37 |
- :default_schedule => agent.default_schedule, |
|
38 |
- :can_receive_events => agent.can_receive_events?, |
|
39 |
- :can_create_events => agent.can_create_events?, |
|
40 |
- :options => agent.default_options, |
|
41 |
- :description_html => agent.html_description |
|
36 |
+ :can_be_scheduled => @agent.can_be_scheduled?, |
|
37 |
+ :default_schedule => @agent.default_schedule, |
|
38 |
+ :can_receive_events => @agent.can_receive_events?, |
|
39 |
+ :can_create_events => @agent.can_create_events?, |
|
40 |
+ :options => @agent.default_options, |
|
41 |
+ :description_html => @agent.html_description, |
|
42 |
+ :form => render_to_string(partial: 'oauth_dropdown') |
|
42 | 43 |
} |
43 | 44 |
end |
44 | 45 |
|
@@ -13,4 +13,27 @@ class ApplicationController < ActionController::Base |
||
13 | 13 |
devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :username, :email, :password, :remember_me) } |
14 | 14 |
devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:username, :email, :password, :password_confirmation, :current_password) } |
15 | 15 |
end |
16 |
+ |
|
17 |
+ def upgrade_warning |
|
18 |
+ return unless current_user |
|
19 |
+ twitter_oauth_check |
|
20 |
+ basecamp_auth_check |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ private |
|
24 |
+ def twitter_oauth_check |
|
25 |
+ if ENV['TWITTER_OAUTH_KEY'].blank? || ENV['TWITTER_OAUTH_SECRET'].blank? |
|
26 |
+ if @twitter_agent = current_user.agents.where("type like 'Agents::Twitter%'").first |
|
27 |
+ @twitter_oauth_key = @twitter_agent.options['consumer_key'].presence || @twitter_agent.credential('twitter_consumer_key') |
|
28 |
+ @twitter_oauth_secret = @twitter_agent.options['consumer_secret'].presence || @twitter_agent.credential('twitter_consumer_secret') |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ def basecamp_auth_check |
|
34 |
+ if ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'].blank? || ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'].blank? |
|
35 |
+ @basecamp_agent = current_user.agents.where(type: 'Agents::BasecampAgent').first |
|
36 |
+ end |
|
37 |
+ end |
|
38 |
+ |
|
16 | 39 |
end |
@@ -1,6 +1,8 @@ |
||
1 | 1 |
class HomeController < ApplicationController |
2 | 2 |
skip_before_filter :authenticate_user! |
3 | 3 |
|
4 |
+ before_filter :upgrade_warning, only: :index |
|
5 |
+ |
|
4 | 6 |
def index |
5 | 7 |
end |
6 | 8 |
|
@@ -45,6 +45,8 @@ class ScenariosController < ApplicationController |
||
45 | 45 |
@exporter = AgentsExporter.new(:name => @scenario.name, |
46 | 46 |
:description => @scenario.description, |
47 | 47 |
:guid => @scenario.guid, |
48 |
+ :tag_fg_color => @scenario.tag_fg_color, |
|
49 |
+ :tag_bg_color => @scenario.tag_bg_color, |
|
48 | 50 |
:source_url => @scenario.public? && export_scenario_url(@scenario), |
49 | 51 |
:agents => @scenario.agents) |
50 | 52 |
response.headers['Content-Disposition'] = 'attachment; filename="' + @exporter.filename + '"' |
@@ -0,0 +1,41 @@ |
||
1 |
+class ServicesController < ApplicationController |
|
2 |
+ before_filter :upgrade_warning, only: :index |
|
3 |
+ |
|
4 |
+ def index |
|
5 |
+ @services = current_user.services.page(params[:page]) |
|
6 |
+ |
|
7 |
+ respond_to do |format| |
|
8 |
+ format.html |
|
9 |
+ format.json { render json: @services } |
|
10 |
+ end |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ def destroy |
|
14 |
+ @services = current_user.services.find(params[:id]) |
|
15 |
+ @services.destroy |
|
16 |
+ |
|
17 |
+ respond_to do |format| |
|
18 |
+ format.html { redirect_to services_path } |
|
19 |
+ format.json { head :no_content } |
|
20 |
+ end |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ def toggle_availability |
|
24 |
+ @service = current_user.services.find(params[:id]) |
|
25 |
+ @service.toggle_availability! |
|
26 |
+ |
|
27 |
+ respond_to do |format| |
|
28 |
+ format.html { redirect_to services_path } |
|
29 |
+ format.json { render json: @service } |
|
30 |
+ end |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ def callback |
|
34 |
+ @service = current_user.services.initialize_or_update_via_omniauth(request.env['omniauth.auth']) |
|
35 |
+ if @service && @service.save |
|
36 |
+ redirect_to services_path, notice: "The service was successfully created." |
|
37 |
+ else |
|
38 |
+ redirect_to services_path, error: "Error creating the service." |
|
39 |
+ end |
|
40 |
+ end |
|
41 |
+end |
@@ -8,7 +8,7 @@ module AgentHelper |
||
8 | 8 |
|
9 | 9 |
def scenario_links(agent) |
10 | 10 |
agent.scenarios.map { |scenario| |
11 |
- link_to(scenario.name, scenario, class: "label label-info") |
|
11 |
+ link_to(scenario.name, scenario, class: "label", style: style_colors(scenario)) |
|
12 | 12 |
}.join(" ").html_safe |
13 | 13 |
end |
14 | 14 |
|
@@ -0,0 +1,23 @@ |
||
1 |
+module ScenarioHelper |
|
2 |
+ |
|
3 |
+ def style_colors(scenario) |
|
4 |
+ colors = { |
|
5 |
+ color: scenario.tag_fg_color || default_scenario_fg_color, |
|
6 |
+ background_color: scenario.tag_bg_color || default_scenario_bg_color |
|
7 |
+ }.map { |key, value| "#{key.to_s.dasherize}:#{value}" }.join(';') |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ def scenario_label(scenario, text = nil) |
|
11 |
+ text ||= scenario.name |
|
12 |
+ content_tag :span, text, class: 'label scenario', style: style_colors(scenario) |
|
13 |
+ end |
|
14 |
+ |
|
15 |
+ def default_scenario_bg_color |
|
16 |
+ '#5BC0DE' |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ def default_scenario_fg_color |
|
20 |
+ '#FFFFFF' |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+end |
@@ -0,0 +1,5 @@ |
||
1 |
+module ServiceHelper |
|
2 |
+ def has_oauth_configuration_for(provider) |
|
3 |
+ ENV["#{provider.upcase}_OAUTH_KEY"].present? && ENV["#{provider.upcase}_OAUTH_SECRET"].present? |
|
4 |
+ end |
|
5 |
+end |
@@ -46,6 +46,7 @@ class Agent < ActiveRecord::Base |
||
46 | 46 |
after_save :possibly_update_event_expirations |
47 | 47 |
|
48 | 48 |
belongs_to :user, :inverse_of => :agents |
49 |
+ belongs_to :service, :inverse_of => :agents |
|
49 | 50 |
has_many :events, -> { order("events.id desc") }, :dependent => :delete_all, :inverse_of => :agent |
50 | 51 |
has_one :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc" |
51 | 52 |
has_many :logs, -> { order("agent_logs.id desc") }, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog" |
@@ -413,7 +414,7 @@ class AgentDrop |
||
413 | 414 |
@object.short_type |
414 | 415 |
end |
415 | 416 |
|
416 |
- METHODS = [ |
|
417 |
+ [ |
|
417 | 418 |
:name, |
418 | 419 |
:type, |
419 | 420 |
:options, |
@@ -426,19 +427,9 @@ class AgentDrop |
||
426 | 427 |
:disabled, |
427 | 428 |
:keep_events_for, |
428 | 429 |
:propagate_immediately, |
429 |
- ] |
|
430 |
- |
|
431 |
- METHODS.each { |attr| |
|
430 |
+ ].each { |attr| |
|
432 | 431 |
define_method(attr) { |
433 | 432 |
@object.__send__(attr) |
434 | 433 |
} unless method_defined?(attr) |
435 | 434 |
} |
436 |
- |
|
437 |
- def each(&block) |
|
438 |
- return to_enum(__method__) unless block |
|
439 |
- |
|
440 |
- METHODS.each { |attr| |
|
441 |
- yield [attr, __sent__(attr)] |
|
442 |
- } |
|
443 |
- end |
|
444 | 435 |
end |
@@ -2,17 +2,18 @@ module Agents |
||
2 | 2 |
class BasecampAgent < Agent |
3 | 3 |
cannot_receive_events! |
4 | 4 |
|
5 |
+ include Oauthable |
|
6 |
+ valid_oauth_providers '37signals' |
|
7 |
+ |
|
5 | 8 |
description <<-MD |
6 | 9 |
The BasecampAgent checks a Basecamp project for new Events |
7 | 10 |
|
8 |
- It is required that you enter your Basecamp credentials (`username` and `password`). |
|
11 |
+ To be able to use this Agent you need to authenticate with 37signals in the [Services](/services) section first. |
|
9 | 12 |
|
10 |
- You also need to provide your Basecamp `user_id` and the `project_id` of the project you want to monitor. |
|
13 |
+ You need to provide the `project_id` of the project you want to monitor. |
|
11 | 14 |
If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows: |
12 | 15 |
|
13 |
- `https://basecamp.com/` |
|
14 |
- user_id |
|
15 |
- `/projects/` |
|
16 |
+ `https://basecamp.com/123456/projects/` |
|
16 | 17 |
project_id |
17 | 18 |
`-explore-basecamp` |
18 | 19 |
MD |
@@ -20,42 +21,36 @@ module Agents |
||
20 | 21 |
event_description <<-MD |
21 | 22 |
Events are the raw JSON provided by the Basecamp API. Should look something like: |
22 | 23 |
|
23 |
- { |
|
24 |
- "creator": { |
|
25 |
- "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3", |
|
26 |
- "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3", |
|
27 |
- "name": "Dominik Sander", |
|
28 |
- "id": 123456 |
|
29 |
- }, |
|
30 |
- "attachments": [], |
|
31 |
- "raw_excerpt": "test test", |
|
32 |
- "excerpt": "test test", |
|
33 |
- "id": 6454342343, |
|
34 |
- "created_at": "2014-04-17T10:25:31.000+02:00", |
|
35 |
- "updated_at": "2014-04-17T10:25:31.000+02:00", |
|
36 |
- "summary": "commented on whaat", |
|
37 |
- "action": "commented on", |
|
38 |
- "target": "whaat", |
|
39 |
- "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json", |
|
40 |
- "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545" |
|
41 |
- } |
|
24 |
+ { |
|
25 |
+ "creator": { |
|
26 |
+ "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3", |
|
27 |
+ "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3", |
|
28 |
+ "name": "Dominik Sander", |
|
29 |
+ "id": 123456 |
|
30 |
+ }, |
|
31 |
+ "attachments": [], |
|
32 |
+ "raw_excerpt": "test test", |
|
33 |
+ "excerpt": "test test", |
|
34 |
+ "id": 6454342343, |
|
35 |
+ "created_at": "2014-04-17T10:25:31.000+02:00", |
|
36 |
+ "updated_at": "2014-04-17T10:25:31.000+02:00", |
|
37 |
+ "summary": "commented on whaat", |
|
38 |
+ "action": "commented on", |
|
39 |
+ "target": "whaat", |
|
40 |
+ "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json", |
|
41 |
+ "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545" |
|
42 |
+ } |
|
42 | 43 |
MD |
43 | 44 |
|
44 | 45 |
default_schedule "every_10m" |
45 | 46 |
|
46 | 47 |
def default_options |
47 | 48 |
{ |
48 |
- 'username' => '', |
|
49 |
- 'password' => '', |
|
50 |
- 'user_id' => '', |
|
51 | 49 |
'project_id' => '', |
52 | 50 |
} |
53 | 51 |
end |
54 | 52 |
|
55 | 53 |
def validate_options |
56 |
- errors.add(:base, "you need to specify your basecamp username") unless options['username'].present? |
|
57 |
- errors.add(:base, "you need to specify your basecamp password") unless options['password'].present? |
|
58 |
- errors.add(:base, "you need to specify your basecamp user id") unless options['user_id'].present? |
|
59 | 54 |
errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present? |
60 | 55 |
end |
61 | 56 |
|
@@ -64,27 +59,29 @@ module Agents |
||
64 | 59 |
end |
65 | 60 |
|
66 | 61 |
def check |
62 |
+ service.prepare_request |
|
67 | 63 |
reponse = HTTParty.get request_url, request_options.merge(query_parameters) |
68 |
- memory[:last_run] = Time.now.utc.iso8601 |
|
69 |
- if last_check_at != nil |
|
70 |
- JSON.parse(reponse.body).each do |event| |
|
64 |
+ events = JSON.parse(reponse.body) |
|
65 |
+ if !memory[:last_event].nil? |
|
66 |
+ events.each do |event| |
|
71 | 67 |
create_event :payload => event |
72 | 68 |
end |
73 | 69 |
end |
70 |
+ memory[:last_event] = events.first['created_at'] if events.length > 0 |
|
74 | 71 |
save! |
75 | 72 |
end |
76 | 73 |
|
77 | 74 |
private |
78 | 75 |
def request_url |
79 |
- "https://basecamp.com/#{URI.encode(interpolated[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
76 |
+ "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
80 | 77 |
end |
81 | 78 |
|
82 | 79 |
def request_options |
83 |
- {:basic_auth => {:username => interpolated[:username], :password => interpolated[:password]}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}} |
|
80 |
+ {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => "Bearer \"#{service.token}\""}} |
|
84 | 81 |
end |
85 | 82 |
|
86 | 83 |
def query_parameters |
87 |
- memory[:last_run].present? ? { :query => {:since => memory[:last_run]} } : {} |
|
84 |
+ memory[:last_event].present? ? { :query => {:since => memory[:last_event]} } : {} |
|
88 | 85 |
end |
89 | 86 |
end |
90 | 87 |
end |
@@ -51,7 +51,7 @@ module Agents |
||
51 | 51 |
{ |
52 | 52 |
"path": "{{date.pretty}}", |
53 | 53 |
"regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
54 |
- "to": "pretty_date", |
|
54 |
+ "to": "pretty_date" |
|
55 | 55 |
} |
56 | 56 |
] |
57 | 57 |
} |
@@ -61,7 +61,7 @@ module Agents |
||
61 | 61 |
"pretty_date": { |
62 | 62 |
"time": "10:00 PM EST", |
63 | 63 |
"0": "10:00 PM EST on January 11, 2013" |
64 |
- "1": "10:00 PM EST", |
|
64 |
+ "1": "10:00 PM EST" |
|
65 | 65 |
} |
66 | 66 |
|
67 | 67 |
So you can use it in `instructions` like this: |
@@ -80,7 +80,19 @@ module Agents |
||
80 | 80 |
} |
81 | 81 |
MD |
82 | 82 |
|
83 |
- event_description "User defined" |
|
83 |
+ event_description do |
|
84 |
+ "Events will have the following fields%s:\n\n %s" % [ |
|
85 |
+ case options['mode'].to_s |
|
86 |
+ when 'merged' |
|
87 |
+ ', merged with the original contents' |
|
88 |
+ when /\{/ |
|
89 |
+ ', conditionally merged with the original contents' |
|
90 |
+ end, |
|
91 |
+ Utils.pretty_print(Hash[options['instructions'].keys.map { |key| |
|
92 |
+ [key, "..."] |
|
93 |
+ }]) |
|
94 |
+ ] |
|
95 |
+ end |
|
84 | 96 |
|
85 | 97 |
after_save :clear_matchers |
86 | 98 |
|
@@ -62,7 +62,7 @@ module Agents |
||
62 | 62 |
.... |
63 | 63 |
}, |
64 | 64 |
'agent_id' => 1234, |
65 |
- 'event_id' => 3432, |
|
65 |
+ 'event_id' => 3432 |
|
66 | 66 |
} |
67 | 67 |
MD |
68 | 68 |
|
@@ -9,11 +9,7 @@ module Agents |
||
9 | 9 |
description <<-MD |
10 | 10 |
The TwitterPublishAgent publishes tweets from the events it receives. |
11 | 11 |
|
12 |
- Twitter credentials must be supplied as either [credentials](/user_credentials) called |
|
13 |
- `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`, |
|
14 |
- or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`. |
|
15 |
- |
|
16 |
- To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). |
|
12 |
+ To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
|
17 | 13 |
|
18 | 14 |
You must also specify a `message` parameter, you can use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the message. |
19 | 15 |
|
@@ -10,11 +10,7 @@ module Agents |
||
10 | 10 |
To follow the Twitter stream, provide an array of `filters`. Multiple words in a filter must all show up in a tweet, but are independent of order. |
11 | 11 |
If you provide an array instead of a filter, the first entry will be considered primary and any additional values will be treated as aliases. |
12 | 12 |
|
13 |
- Twitter credentials must be supplied as either [credentials](/user_credentials) called |
|
14 |
- `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`, |
|
15 |
- or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`. |
|
16 |
- |
|
17 |
- To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). |
|
13 |
+ To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
|
18 | 14 |
|
19 | 15 |
Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent. |
20 | 16 |
|
@@ -9,11 +9,7 @@ module Agents |
||
9 | 9 |
description <<-MD |
10 | 10 |
The TwitterUserAgent follows the timeline of a specified Twitter user. |
11 | 11 |
|
12 |
- Twitter credentials must be supplied as either [credentials](/user_credentials) called |
|
13 |
- `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`, |
|
14 |
- or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`. |
|
15 |
- |
|
16 |
- To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). |
|
12 |
+ To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
|
17 | 13 |
|
18 | 14 |
You must also provide the `username` of the Twitter user to monitor. |
19 | 15 |
|
@@ -42,20 +42,20 @@ module Agents |
||
42 | 42 |
|
43 | 43 |
"extract": { |
44 | 44 |
"word": { "regexp": "^(.+?): (.+)$", index: 1 }, |
45 |
- "definition": { "regexp": "^(.+?): (.+)$", index: 2 }, |
|
45 |
+ "definition": { "regexp": "^(.+?): (.+)$", index: 2 } |
|
46 | 46 |
} |
47 | 47 |
|
48 | 48 |
Or if you prefer names to numbers for index: |
49 | 49 |
|
50 | 50 |
"extract": { |
51 | 51 |
"word": { "regexp": "^(?<word>.+?): (?<definition>.+)$", index: 'word' }, |
52 |
- "definition": { "regexp": "^(?<word>.+?): (?<definition>.+)$", index: 'definition' }, |
|
52 |
+ "definition": { "regexp": "^(?<word>.+?): (?<definition>.+)$", index: 'definition' } |
|
53 | 53 |
} |
54 | 54 |
|
55 | 55 |
To extract the whole content as one event: |
56 | 56 |
|
57 | 57 |
"extract": { |
58 |
- "content": { "regexp": "\A(?m:.)*\z", index: 0 }, |
|
58 |
+ "content": { "regexp": "\A(?m:.)*\z", index: 0 } |
|
59 | 59 |
} |
60 | 60 |
|
61 | 61 |
Beware that `.` does not match the newline character (LF) unless the `m` flag is in effect, and `^`/`$` basically match every line beginning/end. See [this document](http://ruby-doc.org/core-#{RUBY_VERSION}/doc/regexp_rdoc.html) to learn the regular expression variant used in this service. |
@@ -78,7 +78,11 @@ module Agents |
||
78 | 78 |
MD |
79 | 79 |
|
80 | 80 |
event_description do |
81 |
- "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print interpolated['extract']}" |
|
81 |
+ "Events will have the following fields:\n\n %s" % [ |
|
82 |
+ Utils.pretty_print(Hash[options['extract'].keys.map { |key| |
|
83 |
+ [key, "..."] |
|
84 |
+ }]) |
|
85 |
+ ] |
|
82 | 86 |
end |
83 | 87 |
|
84 | 88 |
def working? |
@@ -157,85 +161,60 @@ module Agents |
||
157 | 161 |
log "Storing new result for '#{name}': #{doc.inspect}" |
158 | 162 |
create_event :payload => doc |
159 | 163 |
end |
160 |
- else |
|
161 |
- output = {} |
|
162 |
- interpolated['extract'].each do |name, extraction_details| |
|
163 |
- case extraction_type |
|
164 |
- when "text" |
|
165 |
- regexp = Regexp.new(extraction_details['regexp']) |
|
166 |
- result = [] |
|
167 |
- doc.scan(regexp) { |
|
168 |
- result << Regexp.last_match[extraction_details['index']] |
|
169 |
- } |
|
170 |
- log "Extracting #{extraction_type} at #{regexp}: #{result}" |
|
171 |
- when "json" |
|
172 |
- result = Utils.values_at(doc, extraction_details['path']) |
|
173 |
- log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}" |
|
174 |
- else |
|
175 |
- case |
|
176 |
- when css = extraction_details['css'] |
|
177 |
- nodes = doc.css(css) |
|
178 |
- when xpath = extraction_details['xpath'] |
|
179 |
- doc.remove_namespaces! # ignore xmlns, useful when parsing atom feeds |
|
180 |
- nodes = doc.xpath(xpath) |
|
181 |
- else |
|
182 |
- error '"css" or "xpath" is required for HTML or XML extraction' |
|
183 |
- return |
|
184 |
- end |
|
185 |
- case nodes |
|
186 |
- when Nokogiri::XML::NodeSet |
|
187 |
- result = nodes.map { |node| |
|
188 |
- case value = node.xpath(extraction_details['value']) |
|
189 |
- when Float |
|
190 |
- # Node#xpath() returns any numeric value as float; |
|
191 |
- # convert it to integer as appropriate. |
|
192 |
- value = value.to_i if value.to_i == value |
|
193 |
- end |
|
194 |
- value.to_s |
|
195 |
- } |
|
196 |
- else |
|
197 |
- error "The result of HTML/XML extraction was not a NodeSet" |
|
198 |
- return |
|
199 |
- end |
|
200 |
- log "Extracting #{extraction_type} at #{xpath || css}: #{result}" |
|
201 |
- end |
|
202 |
- output[name] = result |
|
164 |
+ next |
|
165 |
+ end |
|
166 |
+ |
|
167 |
+ output = |
|
168 |
+ case extraction_type |
|
169 |
+ when 'json' |
|
170 |
+ extract_json(doc) |
|
171 |
+ when 'text' |
|
172 |
+ extract_text(doc) |
|
173 |
+ else |
|
174 |
+ extract_xml(doc) |
|
203 | 175 |
end |
204 | 176 |
|
205 |
- num_unique_lengths = interpolated['extract'].keys.map { |name| output[name].length }.uniq |
|
177 |
+ num_unique_lengths = interpolated['extract'].keys.map { |name| output[name].length }.uniq |
|
206 | 178 |
|
207 |
- if num_unique_lengths.length != 1 |
|
208 |
- error "Got an uneven number of matches for #{interpolated['name']}: #{interpolated['extract'].inspect}" |
|
209 |
- return |
|
210 |
- end |
|
179 |
+ if num_unique_lengths.length != 1 |
|
180 |
+ raise "Got an uneven number of matches for #{interpolated['name']}: #{interpolated['extract'].inspect}" |
|
181 |
+ end |
|
211 | 182 |
|
212 |
- old_events = previous_payloads num_unique_lengths.first |
|
213 |
- num_unique_lengths.first.times do |index| |
|
214 |
- result = {} |
|
215 |
- interpolated['extract'].keys.each do |name| |
|
216 |
- result[name] = output[name][index] |
|
217 |
- if name.to_s == 'url' |
|
218 |
- result[name] = (response.env[:url] + result[name]).to_s |
|
219 |
- end |
|
183 |
+ old_events = previous_payloads num_unique_lengths.first |
|
184 |
+ num_unique_lengths.first.times do |index| |
|
185 |
+ result = {} |
|
186 |
+ interpolated['extract'].keys.each do |name| |
|
187 |
+ result[name] = output[name][index] |
|
188 |
+ if name.to_s == 'url' |
|
189 |
+ result[name] = (response.env[:url] + result[name]).to_s |
|
220 | 190 |
end |
191 |
+ end |
|
221 | 192 |
|
222 |
- if store_payload!(old_events, result) |
|
223 |
- log "Storing new parsed result for '#{name}': #{result.inspect}" |
|
224 |
- create_event :payload => result |
|
225 |
- end |
|
193 |
+ if store_payload!(old_events, result) |
|
194 |
+ log "Storing new parsed result for '#{name}': #{result.inspect}" |
|
195 |
+ create_event :payload => result |
|
226 | 196 |
end |
227 | 197 |
end |
228 | 198 |
else |
229 |
- error "Failed: #{response.inspect}" |
|
199 |
+ raise "Failed: #{response.inspect}" |
|
230 | 200 |
end |
231 | 201 |
end |
202 |
+ rescue => e |
|
203 |
+ error e.message |
|
232 | 204 |
end |
233 | 205 |
|
234 | 206 |
def receive(incoming_events) |
235 | 207 |
incoming_events.each do |event| |
208 |
+ Thread.current[:current_event] = event |
|
236 | 209 |
url_to_scrape = event.payload['url'] |
237 | 210 |
check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i |
238 | 211 |
end |
212 |
+ ensure |
|
213 |
+ Thread.current[:current_event] = nil |
|
214 |
+ end |
|
215 |
+ |
|
216 |
+ def interpolated(event = Thread.current[:current_event]) |
|
217 |
+ super |
|
239 | 218 |
end |
240 | 219 |
|
241 | 220 |
private |
@@ -244,22 +223,22 @@ module Agents |
||
244 | 223 |
# If mode is set to 'on_change', this method may return false and update an existing |
245 | 224 |
# event to expire further in the future. |
246 | 225 |
def store_payload!(old_events, result) |
247 |
- if !interpolated['mode'].present? |
|
248 |
- return true |
|
249 |
- elsif interpolated['mode'].to_s == "all" |
|
250 |
- return true |
|
251 |
- elsif interpolated['mode'].to_s == "on_change" |
|
226 |
+ case interpolated['mode'].presence |
|
227 |
+ when 'on_change' |
|
252 | 228 |
result_json = result.to_json |
253 | 229 |
old_events.each do |old_event| |
254 | 230 |
if old_event.payload.to_json == result_json |
255 | 231 |
old_event.expires_at = new_event_expiration_date |
256 | 232 |
old_event.save! |
257 | 233 |
return false |
258 |
- end |
|
234 |
+ end |
|
259 | 235 |
end |
260 |
- return true |
|
236 |
+ true |
|
237 |
+ when 'all', '' |
|
238 |
+ true |
|
239 |
+ else |
|
240 |
+ raise "Illegal options[mode]: #{interpolated['mode']}" |
|
261 | 241 |
end |
262 |
- raise "Illegal options[mode]: " + interpolated['mode'].to_s |
|
263 | 242 |
end |
264 | 243 |
|
265 | 244 |
def previous_payloads(num_events) |
@@ -272,7 +251,7 @@ module Agents |
||
272 | 251 |
look_back = UNIQUENESS_LOOK_BACK |
273 | 252 |
end |
274 | 253 |
end |
275 |
- events.order("id desc").limit(look_back) if interpolated['mode'].present? && interpolated['mode'].to_s == "on_change" |
|
254 |
+ events.order("id desc").limit(look_back) if interpolated['mode'] == "on_change" |
|
276 | 255 |
end |
277 | 256 |
|
278 | 257 |
def extract_full_json? |
@@ -294,27 +273,81 @@ module Agents |
||
294 | 273 |
end).to_s |
295 | 274 |
end |
296 | 275 |
|
276 |
+ def extract_each(doc, &block) |
|
277 |
+ interpolated['extract'].each_with_object({}) { |(name, extraction_details), output| |
|
278 |
+ output[name] = block.call(extraction_details) |
|
279 |
+ } |
|
280 |
+ end |
|
281 |
+ |
|
282 |
+ def extract_json(doc) |
|
283 |
+ extract_each(doc) { |extraction_details| |
|
284 |
+ result = Utils.values_at(doc, extraction_details['path']) |
|
285 |
+ log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}" |
|
286 |
+ result |
|
287 |
+ } |
|
288 |
+ end |
|
289 |
+ |
|
290 |
+ def extract_text(doc) |
|
291 |
+ extract_each(doc) { |extraction_details| |
|
292 |
+ regexp = Regexp.new(extraction_details['regexp']) |
|
293 |
+ result = [] |
|
294 |
+ doc.scan(regexp) { |
|
295 |
+ result << Regexp.last_match[extraction_details['index']] |
|
296 |
+ } |
|
297 |
+ log "Extracting #{extraction_type} at #{regexp}: #{result}" |
|
298 |
+ result |
|
299 |
+ } |
|
300 |
+ end |
|
301 |
+ |
|
302 |
+ def extract_xml(doc) |
|
303 |
+ extract_each(doc) { |extraction_details| |
|
304 |
+ case |
|
305 |
+ when css = extraction_details['css'] |
|
306 |
+ nodes = doc.css(css) |
|
307 |
+ when xpath = extraction_details['xpath'] |
|
308 |
+ doc.remove_namespaces! # ignore xmlns, useful when parsing atom feeds |
|
309 |
+ nodes = doc.xpath(xpath) |
|
310 |
+ else |
|
311 |
+ raise '"css" or "xpath" is required for HTML or XML extraction' |
|
312 |
+ end |
|
313 |
+ case nodes |
|
314 |
+ when Nokogiri::XML::NodeSet |
|
315 |
+ result = nodes.map { |node| |
|
316 |
+ case value = node.xpath(extraction_details['value']) |
|
317 |
+ when Float |
|
318 |
+ # Node#xpath() returns any numeric value as float; |
|
319 |
+ # convert it to integer as appropriate. |
|
320 |
+ value = value.to_i if value.to_i == value |
|
321 |
+ end |
|
322 |
+ value.to_s |
|
323 |
+ } |
|
324 |
+ else |
|
325 |
+ raise "The result of HTML/XML extraction was not a NodeSet" |
|
326 |
+ end |
|
327 |
+ log "Extracting #{extraction_type} at #{xpath || css}: #{result}" |
|
328 |
+ result |
|
329 |
+ } |
|
330 |
+ end |
|
331 |
+ |
|
297 | 332 |
def parse(data) |
298 | 333 |
case extraction_type |
299 |
- when "xml" |
|
300 |
- Nokogiri::XML(data) |
|
301 |
- when "json" |
|
302 |
- JSON.parse(data) |
|
303 |
- when "html" |
|
304 |
- Nokogiri::HTML(data) |
|
305 |
- when "text" |
|
306 |
- data |
|
307 |
- else |
|
308 |
- raise "Unknown extraction type #{extraction_type}" |
|
334 |
+ when "xml" |
|
335 |
+ Nokogiri::XML(data) |
|
336 |
+ when "json" |
|
337 |
+ JSON.parse(data) |
|
338 |
+ when "html" |
|
339 |
+ Nokogiri::HTML(data) |
|
340 |
+ when "text" |
|
341 |
+ data |
|
342 |
+ else |
|
343 |
+ raise "Unknown extraction type #{extraction_type}" |
|
309 | 344 |
end |
310 | 345 |
end |
311 | 346 |
|
312 | 347 |
def is_positive_integer?(value) |
313 |
- begin |
|
314 |
- Integer(value) >= 0 |
|
315 |
- rescue |
|
316 |
- false |
|
317 |
- end |
|
348 |
+ Integer(value) >= 0 |
|
349 |
+ rescue |
|
350 |
+ false |
|
318 | 351 |
end |
319 | 352 |
end |
320 | 353 |
end |
@@ -44,26 +44,21 @@ class Event < ActiveRecord::Base |
||
44 | 44 |
end |
45 | 45 |
|
46 | 46 |
class EventDrop |
47 |
- def initialize(event, payload = event.payload) |
|
48 |
- super(event) |
|
49 |
- @payload = payload |
|
50 |
- end |
|
51 |
- |
|
52 |
- def before_method(key) |
|
53 |
- if @payload.key?(key) |
|
54 |
- @payload[key] |
|
55 |
- else |
|
56 |
- case key |
|
57 |
- when 'agent' |
|
58 |
- @object.agent |
|
59 |
- when 'created_at' |
|
60 |
- @object.created_at |
|
61 |
- end |
|
62 |
- end |
|
47 |
+ def initialize(object, locals = nil) |
|
48 |
+ locals = object.payload.merge(locals || {}) |
|
49 |
+ super |
|
63 | 50 |
end |
64 | 51 |
|
65 | 52 |
def each(&block) |
66 | 53 |
return to_enum(__method__) unless block |
67 |
- @payload.each(&block) |
|
54 |
+ @locals.each(&block) |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ def agent |
|
58 |
+ @object.agent |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ def created_at |
|
62 |
+ @object.created_at |
|
68 | 63 |
end |
69 | 64 |
end |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
class Scenario < ActiveRecord::Base |
2 | 2 |
include HasGuid |
3 | 3 |
|
4 |
- attr_accessible :name, :agent_ids, :description, :public, :source_url |
|
4 |
+ attr_accessible :name, :agent_ids, :description, :public, :source_url, :tag_fg_color, :tag_bg_color |
|
5 | 5 |
|
6 | 6 |
belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios |
7 | 7 |
has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario |
@@ -9,6 +9,11 @@ class Scenario < ActiveRecord::Base |
||
9 | 9 |
|
10 | 10 |
validates_presence_of :name, :user |
11 | 11 |
|
12 |
+ validates_format_of :tag_fg_color, :tag_bg_color, |
|
13 |
+ # Regex adapted from: http://stackoverflow.com/a/1636354/3130625 |
|
14 |
+ :with => /\A#(?:[0-9a-fA-F]{3}){1,2}\z/, :allow_nil => true, |
|
15 |
+ :message => "must be a valid hex color." |
|
16 |
+ |
|
12 | 17 |
validate :agents_are_owned |
13 | 18 |
|
14 | 19 |
protected |
@@ -60,10 +60,14 @@ class ScenarioImport |
||
60 | 60 |
description = parsed_data['description'] |
61 | 61 |
name = parsed_data['name'] |
62 | 62 |
links = parsed_data['links'] |
63 |
+ tag_fg_color = parsed_data['tag_fg_color'] |
|
64 |
+ tag_bg_color = parsed_data['tag_bg_color'] |
|
63 | 65 |
source_url = parsed_data['source_url'].presence || nil |
64 | 66 |
@scenario = user.scenarios.where(:guid => guid).first_or_initialize |
65 | 67 |
@scenario.update_attributes!(:name => name, :description => description, |
66 |
- :source_url => source_url, :public => false) |
|
68 |
+ :source_url => source_url, :public => false, |
|
69 |
+ :tag_fg_color => tag_fg_color, |
|
70 |
+ :tag_bg_color => tag_bg_color) |
|
67 | 71 |
|
68 | 72 |
unless options[:skip_agents] |
69 | 73 |
created_agents = agent_diffs.map do |agent_diff| |
@@ -76,17 +80,19 @@ class ScenarioImport |
||
76 | 80 |
agent.schedule = agent_diff.schedule.updated if agent_diff.schedule.present? |
77 | 81 |
agent.keep_events_for = agent_diff.keep_events_for.updated if agent_diff.keep_events_for.present? |
78 | 82 |
agent.propagate_immediately = agent_diff.propagate_immediately.updated if agent_diff.propagate_immediately.present? # == "true" |
83 |
+ agent.service_id = agent_diff.service_id.updated if agent_diff.service_id.present? |
|
79 | 84 |
unless agent.save |
80 | 85 |
success = false |
81 | 86 |
errors.add(:base, "Errors when saving '#{agent_diff.name.incoming}': #{agent.errors.full_messages.to_sentence}") |
82 | 87 |
end |
83 | 88 |
agent |
84 | 89 |
end |
85 |
- |
|
86 |
- links.each do |link| |
|
87 |
- receiver = created_agents[link['receiver']] |
|
88 |
- source = created_agents[link['source']] |
|
89 |
- receiver.sources << source unless receiver.sources.include?(source) |
|
90 |
+ if success |
|
91 |
+ links.each do |link| |
|
92 |
+ receiver = created_agents[link['receiver']] |
|
93 |
+ source = created_agents[link['source']] |
|
94 |
+ receiver.sources << source unless receiver.sources.include?(source) |
|
95 |
+ end |
|
90 | 96 |
end |
91 | 97 |
end |
92 | 98 |
|
@@ -149,6 +155,9 @@ class ScenarioImport |
||
149 | 155 |
errors.add(:base, "Your updated options for '#{agent_data['name']}' were unparsable.") |
150 | 156 |
end |
151 | 157 |
end |
158 |
+ if agent_diff.requires_service? && merges.present? && merges[index.to_s].present? && merges[index.to_s]['service_id'].present? |
|
159 |
+ agent_diff.service_id = AgentDiff::FieldDiff.new(merges[index.to_s]['service_id'].to_i) |
|
160 |
+ end |
|
152 | 161 |
agent_diff |
153 | 162 |
end |
154 | 163 |
end |
@@ -192,6 +201,10 @@ class ScenarioImport |
||
192 | 201 |
@requires_merge |
193 | 202 |
end |
194 | 203 |
|
204 |
+ def requires_service? |
|
205 |
+ !!agent_instance.try(:oauthable?) |
|
206 |
+ end |
|
207 |
+ |
|
195 | 208 |
def store!(agent_data) |
196 | 209 |
self.type = FieldDiff.new(agent_data["type"].split("::").pop) |
197 | 210 |
self.options = FieldDiff.new(agent_data['options'] || {}) |
@@ -252,5 +265,9 @@ class ScenarioImport |
||
252 | 265 |
key.gsub(/[^a-zA-Z0-9_-]/, '') |
253 | 266 |
end |
254 | 267 |
end |
268 |
+ |
|
269 |
+ def agent_instance |
|
270 |
+ "Agents::#{self.type.updated}".constantize.new |
|
271 |
+ end |
|
255 | 272 |
end |
256 | 273 |
end |
@@ -0,0 +1,89 @@ |
||
1 |
+class Service < ActiveRecord::Base |
|
2 |
+ PROVIDER_TO_ENV_MAP = {'37signals' => 'THIRTY_SEVEN_SIGNALS'} |
|
3 |
+ |
|
4 |
+ attr_accessible :provider, :name, :token, :secret, :refresh_token, :expires_at, :global, :options, :uid |
|
5 |
+ |
|
6 |
+ serialize :options, Hash |
|
7 |
+ |
|
8 |
+ belongs_to :user, :inverse_of => :services |
|
9 |
+ has_many :agents, :inverse_of => :service |
|
10 |
+ |
|
11 |
+ validates_presence_of :user_id, :provider, :name, :token |
|
12 |
+ |
|
13 |
+ before_destroy :disable_agents |
|
14 |
+ |
|
15 |
+ scope :available_to_user, lambda { |user| where("services.user_id = ? or services.global = true", user.id) } |
|
16 |
+ scope :by_name, lambda { |dir = 'desc'| order("services.name #{dir}") } |
|
17 |
+ |
|
18 |
+ def disable_agents(conditions = {}) |
|
19 |
+ agents.where.not(conditions[:where_not] || {}).each do |agent| |
|
20 |
+ agent.service_id = nil |
|
21 |
+ agent.disabled = true |
|
22 |
+ agent.save!(validate: false) |
|
23 |
+ end |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ def toggle_availability! |
|
27 |
+ disable_agents(where_not: {user_id: self.user_id}) if global |
|
28 |
+ self.global = !self.global |
|
29 |
+ self.save! |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def prepare_request |
|
33 |
+ if expires_at && Time.now > expires_at |
|
34 |
+ refresh_token! |
|
35 |
+ end |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ def refresh_token! |
|
39 |
+ response = HTTParty.post(endpoint, query: { |
|
40 |
+ type: 'refresh', |
|
41 |
+ client_id: oauth_key, |
|
42 |
+ client_secret: oauth_secret, |
|
43 |
+ refresh_token: refresh_token |
|
44 |
+ }) |
|
45 |
+ data = JSON.parse(response.body) |
|
46 |
+ update(expires_at: Time.now + data['expires_in'], token: data['access_token'], refresh_token: data['refresh_token'].presence || refresh_token) |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ def endpoint |
|
50 |
+ client_options = "OmniAuth::Strategies::#{OmniAuth::Utils.camelize(self.provider)}".constantize.default_options['client_options'] |
|
51 |
+ URI.join(client_options['site'], client_options['token_url']) |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ def provider_to_env |
|
55 |
+ PROVIDER_TO_ENV_MAP[provider].presence || provider.upcase |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def oauth_key |
|
59 |
+ ENV["#{provider_to_env}_OAUTH_KEY"] |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ def oauth_secret |
|
63 |
+ ENV["#{provider_to_env}_OAUTH_SECRET"] |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ def self.provider_specific_options(omniauth) |
|
67 |
+ case omniauth['provider'] |
|
68 |
+ when 'twitter', 'github' |
|
69 |
+ { name: omniauth['info']['nickname'] } |
|
70 |
+ when '37signals' |
|
71 |
+ { user_id: omniauth['extra']['accounts'][0]['id'], name: omniauth['info']['name'] } |
|
72 |
+ else |
|
73 |
+ { name: omniauth['info']['nickname'] } |
|
74 |
+ end |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ def self.initialize_or_update_via_omniauth(omniauth) |
|
78 |
+ options = provider_specific_options(omniauth) |
|
79 |
+ |
|
80 |
+ find_or_initialize_by(provider: omniauth['provider'], uid: omniauth['uid'].to_s).tap do |service| |
|
81 |
+ service.assign_attributes token: omniauth['credentials']['token'], |
|
82 |
+ secret: omniauth['credentials']['secret'], |
|
83 |
+ name: options[:name], |
|
84 |
+ refresh_token: omniauth['credentials']['refresh_token'], |
|
85 |
+ expires_at: omniauth['credentials']['expires_at'] && Time.at(omniauth['credentials']['expires_at']), |
|
86 |
+ options: options |
|
87 |
+ end |
|
88 |
+ end |
|
89 |
+end |
@@ -27,6 +27,11 @@ class User < ActiveRecord::Base |
||
27 | 27 |
has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user |
28 | 28 |
has_many :logs, :through => :agents, :class_name => "AgentLog" |
29 | 29 |
has_many :scenarios, :inverse_of => :user, :dependent => :destroy |
30 |
+ has_many :services, -> { by_name('asc') }, :dependent => :destroy |
|
31 |
+ |
|
32 |
+ def available_services |
|
33 |
+ Service.available_to_user(self).by_name |
|
34 |
+ end |
|
30 | 35 |
|
31 | 36 |
# Allow users to login via either email or username. |
32 | 37 |
def self.find_first_by_auth_conditions(warden_conditions) |
@@ -32,7 +32,7 @@ |
||
32 | 32 |
|
33 | 33 |
<% agent.scenarios.each do |scenario| %> |
34 | 34 |
<li> |
35 |
- <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from <span class='scenario label label-info'>#{h scenario.name}</span>".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %> |
|
35 |
+ <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from #{scenario_label(scenario)}".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %> |
|
36 | 36 |
</li> |
37 | 37 |
<% end %> |
38 | 38 |
<% end %> |
@@ -25,11 +25,15 @@ |
||
25 | 25 |
</div> |
26 | 26 |
<% end %> |
27 | 27 |
|
28 |
- <div class="form-group"> |
|
28 |
+ <div class="form-group type-select"> |
|
29 | 29 |
<%= f.label :name %> |
30 | 30 |
<%= f.text_field :name, :class => 'form-control' %> |
31 | 31 |
</div> |
32 | 32 |
|
33 |
+ <div class='oauthable-form'> |
|
34 |
+ <%= render partial: 'oauth_dropdown' %> |
|
35 |
+ </div> |
|
36 |
+ |
|
33 | 37 |
<div class="form-group"> |
34 | 38 |
<%= f.label :schedule, :class => 'control-label' %> |
35 | 39 |
<div class="schedule-region" data-can-be-scheduled="<%= @agent.can_be_scheduled? %>"> |
@@ -0,0 +1,6 @@ |
||
1 |
+<% if @agent.try(:oauthable?) %> |
|
2 |
+ <div class="form-group type-select"> |
|
3 |
+ <%= label_tag :service %> |
|
4 |
+ <%= select_tag 'agent[service_id]', options_for_select(@agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, @agent.service_id), class: 'form-control' %> |
|
5 |
+ </div> |
|
6 |
+<% end %> |
@@ -0,0 +1,26 @@ |
||
1 |
+<% if @twitter_agent || @basecamp_agent %> |
|
2 |
+ <div class="alert alert-danger" role="alert"> |
|
3 |
+ <p> |
|
4 |
+ <b>Warning!</b> You need to update your Huginn configuration, so your agents continue to work with the new OAuth services. |
|
5 |
+ </p> |
|
6 |
+ <br/> |
|
7 |
+ <% if @twitter_agent %> |
|
8 |
+ <p> |
|
9 |
+ To complete the migration of your <b>Twitter</b> agents you need to update your .env file and add the following two lines: |
|
10 |
+ |
|
11 |
+ <pre> |
|
12 |
+TWITTER_OAUTH_KEY=<%= @twitter_oauth_key %> |
|
13 |
+TWITTER_OAUTH_SECRET=<%= @twitter_oauth_secret %> |
|
14 |
+ </pre> |
|
15 |
+ To authenticate new accounts with your twitter OAuth application you need to log in the to <a href="https://apps.twitter.com/" target="_blank">twitter application management page</a> and set the callback URL of your application to "http<%= ENV['FORCE_SSL'] == 'true' ? 's' : '' %>://<%= ENV['DOMAIN'] %>/auth/twitter/callback". |
|
16 |
+ |
|
17 |
+ </p> |
|
18 |
+ <% end %> |
|
19 |
+ <% if @basecamp_agent %> |
|
20 |
+ <p> |
|
21 |
+ Your <b>Basecamp</b> agents could not be migrated automatically. You need to manually register an application with 37signals and authenticate Huginn to use it.<br/> |
|
22 |
+ Have a look at the <%= link_to 'Wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: '_blank' %> if you need help. |
|
23 |
+ </p> |
|
24 |
+ <% end %> |
|
25 |
+ </div> |
|
26 |
+<% end -%> |
@@ -22,6 +22,7 @@ |
||
22 | 22 |
<%= nav_link "Scenarios", scenarios_path %> |
23 | 23 |
<%= nav_link "Events", events_path %> |
24 | 24 |
<%= nav_link "Credentials", user_credentials_path %> |
25 |
+ <%= nav_link "Services", services_path %> |
|
25 | 26 |
</ul> |
26 | 27 |
<% end %> |
27 | 28 |
|
@@ -24,7 +24,10 @@ |
||
24 | 24 |
<%= render 'layouts/messages' %> |
25 | 25 |
</div> |
26 | 26 |
</div> |
27 |
- |
|
27 |
+ <% if user_signed_in? %> |
|
28 |
+ <%= render "upgrade_warning" %> |
|
29 |
+ <% end %> |
|
30 |
+ |
|
28 | 31 |
<%= yield %> |
29 | 32 |
|
30 | 33 |
</div> |
@@ -13,9 +13,8 @@ |
||
13 | 13 |
<div class="alert alert-warning"> |
14 | 14 |
<span class='glyphicon glyphicon-warning-sign'></span> |
15 | 15 |
This Scenario already exists in your system. The import will update your existing |
16 |
- <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario's title |
|
17 |
- and |
|
18 |
- description. Below you can customize how the individual agents get updated. |
|
16 |
+ <%= scenario_label(@scenario_import.existing_scenario) %> Scenario's title, |
|
17 |
+ description and tag colors. Below you can customize how the individual agents get updated. |
|
19 | 18 |
</div> |
20 | 19 |
<% end %> |
21 | 20 |
|
@@ -120,6 +119,17 @@ |
||
120 | 119 |
</div> |
121 | 120 |
<% end %> |
122 | 121 |
</div> |
122 |
+ |
|
123 |
+ <% if agent_diff.requires_service? %> |
|
124 |
+ <div class='row'> |
|
125 |
+ <div class='col-md-4'> |
|
126 |
+ <div class="form-group type-select"> |
|
127 |
+ <%= label_tag "scenario_import[merges][#{index}][service_id]", 'Service' %> |
|
128 |
+ <%= select_tag "scenario_import[merges][#{index}][service_id]", options_for_select(agent_diff.agent_instance.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent_diff.agent.try(:service_id)), class: 'form-control' %> |
|
129 |
+ </div> |
|
130 |
+ </div> |
|
131 |
+ </div> |
|
132 |
+ <% end %> |
|
123 | 133 |
</div> |
124 | 134 |
<% end %> |
125 | 135 |
</div> |
@@ -15,6 +15,18 @@ |
||
15 | 15 |
<%= f.text_field :name, :class => 'form-control', :placeholder => "Name your Scenario" %> |
16 | 16 |
</div> |
17 | 17 |
</div> |
18 |
+ <div class="col-md-2"> |
|
19 |
+ <div class="form-group"> |
|
20 |
+ <%= f.label :tag_bg_color, "Tag Background Color" %> |
|
21 |
+ <%= f.color_field :tag_bg_color, :class => 'form-control', :value => @scenario.tag_bg_color || default_scenario_bg_color %> |
|
22 |
+ </div> |
|
23 |
+ </div> |
|
24 |
+ <div class="col-md-2"> |
|
25 |
+ <div class="form-group"> |
|
26 |
+ <%= f.label :tag_fg_color, "Tag Foreground Color" %> |
|
27 |
+ <%= f.color_field :tag_fg_color, :class => 'form-control', :value => @scenario.tag_fg_color || default_scenario_fg_color %> |
|
28 |
+ </div> |
|
29 |
+ </div> |
|
18 | 30 |
</div> |
19 | 31 |
|
20 | 32 |
<div class="row"> |
@@ -54,4 +66,4 @@ |
||
54 | 66 |
</div> |
55 | 67 |
</div> |
56 | 68 |
</div> |
57 |
-<% end %> |
|
69 |
+<% end %> |
@@ -21,6 +21,7 @@ |
||
21 | 21 |
<% @scenarios.each do |scenario| %> |
22 | 22 |
<tr> |
23 | 23 |
<td> |
24 |
+ <%= scenario_label(scenario, content_tag(:i, '', class: 'glyphicon glyphicon-font')) %> |
|
24 | 25 |
<%= link_to(scenario.name, scenario) %> |
25 | 26 |
</td> |
26 | 27 |
<td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td> |
@@ -47,4 +48,4 @@ |
||
47 | 48 |
</div> |
48 | 49 |
</div> |
49 | 50 |
</div> |
50 |
-</div> |
|
51 |
+</div> |
@@ -2,7 +2,7 @@ |
||
2 | 2 |
<div class='row'> |
3 | 3 |
<div class='col-md-12'> |
4 | 4 |
<div class="page-header"> |
5 |
- <h2>Share <span class='label label-info scenario'><%= @scenario.name %></span> with the world</h2> |
|
5 |
+ <h2>Share <%= scenario_label(@scenario) %> with the world</h2> |
|
6 | 6 |
</div> |
7 | 7 |
|
8 | 8 |
<p> |
@@ -30,4 +30,4 @@ |
||
30 | 30 |
</div> |
31 | 31 |
</div> |
32 | 32 |
</div> |
33 |
-</div> |
|
33 |
+</div> |
@@ -2,7 +2,8 @@ |
||
2 | 2 |
<div class='row'> |
3 | 3 |
<div class='col-md-12'> |
4 | 4 |
<div class="page-header"> |
5 |
- <h2><span class='label label-info scenario'><%= @scenario.name %></span> <%= "Public" if @scenario.public? %> Scenario</h2> |
|
5 |
+ <h2><%= scenario_label(@scenario) %> <%= "Public" if @scenario.public? %> Scenario</h2> |
|
6 |
+ |
|
6 | 7 |
</div> |
7 | 8 |
|
8 | 9 |
<% if @scenario.description.present? %> |
@@ -0,0 +1,58 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2> |
|
6 |
+ Your Services |
|
7 |
+ </h2> |
|
8 |
+ </div> |
|
9 |
+ <p> |
|
10 |
+ Before you can authenticate with a service, you need to set it up. Have a look at the Huginn |
|
11 |
+ <%= link_to 'wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: :_blank %> |
|
12 |
+ for guidance. |
|
13 |
+ </p> |
|
14 |
+ <% if has_oauth_configuration_for('twitter') %> |
|
15 |
+ <p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p> |
|
16 |
+ <% end %> |
|
17 |
+ <% if has_oauth_configuration_for('thirty_seven_signals') %> |
|
18 |
+ <p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p> |
|
19 |
+ <% end -%> |
|
20 |
+ <% if has_oauth_configuration_for('github') %> |
|
21 |
+ <p><%= link_to "Authenticate with Github", "/auth/github" %></p> |
|
22 |
+ <% end -%> |
|
23 |
+ <hr> |
|
24 |
+ |
|
25 |
+ <div class='table-responsive'> |
|
26 |
+ <table class='table table-striped events'> |
|
27 |
+ <tr> |
|
28 |
+ <th>Provider</th> |
|
29 |
+ <th>Username</th> |
|
30 |
+ <th>Global?</th> |
|
31 |
+ <th></th> |
|
32 |
+ </tr> |
|
33 |
+ |
|
34 |
+ <% @services.each do |service| %> |
|
35 |
+ <tr> |
|
36 |
+ <td><%= service.provider %></td> |
|
37 |
+ <td><%= service.name %></td> |
|
38 |
+ <td><%= service.global ? 'Yes' : 'No' %></td> |
|
39 |
+ <td> |
|
40 |
+ <div class="btn-group btn-group-xs"> |
|
41 |
+ <% if service.global %> |
|
42 |
+ <%= link_to 'Make private', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to remove access to your data on this service for other users?'}, class: "btn btn-default" %> |
|
43 |
+ <% else %> |
|
44 |
+ <%= link_to 'Make global', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to grant every user on this system access to your data on this service?'}, class: "btn btn-default" %> |
|
45 |
+ <% end %> |
|
46 |
+ <%= link_to 'Delete', service_path(service), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default btn-danger" %> |
|
47 |
+ </div> |
|
48 |
+ </td> |
|
49 |
+ </tr> |
|
50 |
+ <% end %> |
|
51 |
+ </table> |
|
52 |
+ </div> |
|
53 |
+ |
|
54 |
+ <%= paginate @services, :theme => 'twitter-bootstrap-3' %> |
|
55 |
+ </div> |
|
56 |
+ </div> |
|
57 |
+</div> |
|
58 |
+ |
@@ -5,14 +5,14 @@ |
||
5 | 5 |
</head> |
6 | 6 |
<body> |
7 | 7 |
<% if @headline %> |
8 |
- <h1><%= @headline %></h1> |
|
8 |
+ <h1><%= sanitize @headline %></h1> |
|
9 | 9 |
<% end %> |
10 | 10 |
<% @groups.each do |group| %> |
11 | 11 |
<div style='margin-bottom: 10px;'> |
12 |
- <div><%= group[:title] %></div> |
|
12 |
+ <div><%= sanitize group[:title] %></div> |
|
13 | 13 |
<% group[:entries].each do |entry| %> |
14 | 14 |
<div style='margin-left: 10px;'> |
15 |
- <%= entry %> |
|
15 |
+ <%= sanitize entry %> |
|
16 | 16 |
</div> |
17 | 17 |
<% end %> |
18 | 18 |
</div> |
@@ -0,0 +1,5 @@ |
||
1 |
+Rails.application.config.middleware.use OmniAuth::Builder do |
|
2 |
+ provider :twitter, ENV['TWITTER_OAUTH_KEY'], ENV['TWITTER_OAUTH_SECRET'], authorize_params: {force_login: 'true', use_authorize: 'true'} |
|
3 |
+ provider '37signals', ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'], ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'] |
|
4 |
+ provider :github, ENV['GITHUB_OAUTH_KEY'], ENV['GITHUB_OAUTH_SECRET'] |
|
5 |
+end |
@@ -45,6 +45,12 @@ Huginn::Application.routes.draw do |
||
45 | 45 |
|
46 | 46 |
resources :user_credentials, :except => :show |
47 | 47 |
|
48 |
+ resources :services, :only => [:index, :destroy] do |
|
49 |
+ member do |
|
50 |
+ post :toggle_availability |
|
51 |
+ end |
|
52 |
+ end |
|
53 |
+ |
|
48 | 54 |
get "/worker_status" => "worker_status#show" |
49 | 55 |
|
50 | 56 |
post "/users/:user_id/update_location/:secret" => "user_location_updates#create" |
@@ -56,6 +62,7 @@ Huginn::Application.routes.draw do |
||
56 | 62 |
# get "/delayed_job" => DelayedJobWeb, :anchor => false |
57 | 63 |
|
58 | 64 |
devise_for :users, :sign_out_via => [ :post, :delete ] |
65 |
+ get '/auth/:provider/callback', to: 'services#callback' |
|
59 | 66 |
|
60 | 67 |
get "/about" => "home#about" |
61 | 68 |
root :to => "home#index" |
@@ -0,0 +1,18 @@ |
||
1 |
+class CreateServices < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ create_table :services do |t| |
|
4 |
+ t.integer :user_id, null: false |
|
5 |
+ t.string :provider, null: false |
|
6 |
+ t.string :name, null: false |
|
7 |
+ t.text :token, null: false |
|
8 |
+ t.text :secret |
|
9 |
+ t.text :refresh_token |
|
10 |
+ t.datetime :expires_at |
|
11 |
+ t.boolean :global, default: false |
|
12 |
+ t.text :options |
|
13 |
+ t.timestamps |
|
14 |
+ end |
|
15 |
+ add_index :services, :user_id |
|
16 |
+ add_index :services, [:user_id, :global] |
|
17 |
+ end |
|
18 |
+end |
@@ -0,0 +1,5 @@ |
||
1 |
+class AddServiceIdToAgents < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :agents, :service_id, :integer |
|
4 |
+ end |
|
5 |
+end |
@@ -0,0 +1,61 @@ |
||
1 |
+class MigrateAgentsToServiceAuthentication < ActiveRecord::Migration |
|
2 |
+ def twitter_consumer_key(agent) |
|
3 |
+ agent.options['consumer_key'].presence || agent.credential('twitter_consumer_key') |
|
4 |
+ end |
|
5 |
+ |
|
6 |
+ def twitter_consumer_secret(agent) |
|
7 |
+ agent.options['consumer_secret'].presence || agent.credential('twitter_consumer_secret') |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ def twitter_oauth_token(agent) |
|
11 |
+ agent.options['oauth_token'].presence || agent.options['access_key'].presence || agent.credential('twitter_oauth_token') |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ def twitter_oauth_token_secret(agent) |
|
15 |
+ agent.options['oauth_token_secret'].presence || agent.options['access_secret'].presence || agent.credential('twitter_oauth_token_secret') |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ def up |
|
19 |
+ agents = Agent.where(type: ['Agents::TwitterUserAgent', 'Agents::TwitterStreamAgent', 'Agents::TwitterPublishAgent']).each do |agent| |
|
20 |
+ service = agent.user.services.create!( |
|
21 |
+ provider: 'twitter', |
|
22 |
+ name: "Migrated '#{agent.name}'", |
|
23 |
+ token: twitter_oauth_token(agent), |
|
24 |
+ secret: twitter_oauth_token_secret(agent) |
|
25 |
+ ) |
|
26 |
+ agent.service_id = service.id |
|
27 |
+ agent.save!(validate: false) |
|
28 |
+ end |
|
29 |
+ migrated = false |
|
30 |
+ if agents.length > 0 |
|
31 |
+ puts <<-EOF.strip_heredoc |
|
32 |
+ |
|
33 |
+ Your Twitter agents were successfully migrated. You need to update your .env file and add the following two lines: |
|
34 |
+ |
|
35 |
+ TWITTER_OAUTH_KEY=#{twitter_consumer_key(agents.first)} |
|
36 |
+ TWITTER_OAUTH_SECRET=#{twitter_consumer_secret(agents.first)} |
|
37 |
+ |
|
38 |
+ To authenticate new accounts with your twitter OAuth application you need to log in the to twitter application management page (https://apps.twitter.com/) |
|
39 |
+ and set the callback URL of your application to "http#{ENV['FORCE_SSL'] == 'true' ? 's' : ''}://#{ENV['DOMAIN']}/auth/twitter/callback" |
|
40 |
+ |
|
41 |
+ EOF |
|
42 |
+ migrated = true |
|
43 |
+ end |
|
44 |
+ if Agent.where(type: ['Agents::BasecampAgent']).count > 0 |
|
45 |
+ puts <<-EOF.strip_heredoc |
|
46 |
+ |
|
47 |
+ Your Basecamp agents can not be migrated automatically. You need to manually register an application with 37signals and authenticate Huginn to use it. |
|
48 |
+ Have a look at the wiki (https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications) if you need help. |
|
49 |
+ |
|
50 |
+ |
|
51 |
+ EOF |
|
52 |
+ migrated = true |
|
53 |
+ end |
|
54 |
+ sleep 20 if migrated |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ def down |
|
58 |
+ raise ActiveRecord::IrreversibleMigration, "Cannot revert migration to OAuth services" |
|
59 |
+ end |
|
60 |
+end |
|
61 |
+ |
@@ -0,0 +1,5 @@ |
||
1 |
+class RemoveServiceIndexOnUserId < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ remove_index :services, :user_id |
|
4 |
+ end |
|
5 |
+end |
@@ -0,0 +1,7 @@ |
||
1 |
+class AddUidColumnToServices < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :services, :uid, :string |
|
4 |
+ add_index :services, :uid |
|
5 |
+ add_index :services, :provider |
|
6 |
+ end |
|
7 |
+end |
@@ -0,0 +1,6 @@ |
||
1 |
+class AddTagColorToScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :scenarios, :tag_bg_color, :string |
|
4 |
+ add_column :scenarios, :tag_fg_color, :string |
|
5 |
+ end |
|
6 |
+end |
@@ -13,19 +13,22 @@ |
||
13 | 13 |
|
14 | 14 |
ActiveRecord::Schema.define(version: 20140822085519) do |
15 | 15 |
|
16 |
+ # These are extensions that must be enabled in order to support this database |
|
17 |
+ enable_extension "plpgsql" |
|
18 |
+ |
|
16 | 19 |
create_table "agent_logs", force: true do |t| |
17 |
- t.integer "agent_id", null: false |
|
18 |
- t.text "message", limit: 16777215, null: false |
|
19 |
- t.integer "level", default: 3, null: false |
|
20 |
+ t.integer "agent_id", null: false |
|
21 |
+ t.text "message", null: false |
|
22 |
+ t.integer "level", default: 3, null: false |
|
20 | 23 |
t.integer "inbound_event_id" |
21 | 24 |
t.integer "outbound_event_id" |
22 |
- t.datetime "created_at", null: false |
|
23 |
- t.datetime "updated_at", null: false |
|
25 |
+ t.datetime "created_at" |
|
26 |
+ t.datetime "updated_at" |
|
24 | 27 |
end |
25 | 28 |
|
26 | 29 |
create_table "agents", force: true do |t| |
27 | 30 |
t.integer "user_id" |
28 |
- t.text "options", limit: 16777215 |
|
31 |
+ t.text "options" |
|
29 | 32 |
t.string "type" |
30 | 33 |
t.string "name" |
31 | 34 |
t.string "schedule" |
@@ -33,16 +36,17 @@ ActiveRecord::Schema.define(version: 20140822085519) do |
||
33 | 36 |
t.datetime "last_check_at" |
34 | 37 |
t.datetime "last_receive_at" |
35 | 38 |
t.integer "last_checked_event_id" |
36 |
- t.datetime "created_at", null: false |
|
37 |
- t.datetime "updated_at", null: false |
|
38 |
- t.text "memory", limit: 2147483647 |
|
39 |
+ t.datetime "created_at" |
|
40 |
+ t.datetime "updated_at" |
|
41 |
+ t.text "memory" |
|
39 | 42 |
t.datetime "last_web_request_at" |
43 |
+ t.integer "keep_events_for", default: 0, null: false |
|
40 | 44 |
t.datetime "last_event_at" |
41 | 45 |
t.datetime "last_error_log_at" |
42 |
- t.integer "keep_events_for", default: 0, null: false |
|
43 |
- t.boolean "propagate_immediately", default: false, null: false |
|
44 |
- t.boolean "disabled", default: false, null: false |
|
45 |
- t.string "guid", null: false |
|
46 |
+ t.boolean "propagate_immediately", default: false, null: false |
|
47 |
+ t.boolean "disabled", default: false, null: false |
|
48 |
+ t.string "guid", null: false |
|
49 |
+ t.integer "service_id" |
|
46 | 50 |
end |
47 | 51 |
|
48 | 52 |
add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree |
@@ -61,17 +65,17 @@ ActiveRecord::Schema.define(version: 20140822085519) do |
||
61 | 65 |
add_index "chains", ["target_id"], name: "index_chains_on_target_id", using: :btree |
62 | 66 |
|
63 | 67 |
create_table "delayed_jobs", force: true do |t| |
64 |
- t.integer "priority", default: 0 |
|
65 |
- t.integer "attempts", default: 0 |
|
66 |
- t.text "handler", limit: 16777215 |
|
67 |
- t.text "last_error", limit: 16777215 |
|
68 |
+ t.integer "priority", default: 0 |
|
69 |
+ t.integer "attempts", default: 0 |
|
70 |
+ t.text "handler" |
|
71 |
+ t.text "last_error" |
|
68 | 72 |
t.datetime "run_at" |
69 | 73 |
t.datetime "locked_at" |
70 | 74 |
t.datetime "failed_at" |
71 | 75 |
t.string "locked_by" |
72 | 76 |
t.string "queue" |
73 |
- t.datetime "created_at", null: false |
|
74 |
- t.datetime "updated_at", null: false |
|
77 |
+ t.datetime "created_at" |
|
78 |
+ t.datetime "updated_at" |
|
75 | 79 |
end |
76 | 80 |
|
77 | 81 |
add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree |
@@ -79,11 +83,11 @@ ActiveRecord::Schema.define(version: 20140822085519) do |
||
79 | 83 |
create_table "events", force: true do |t| |
80 | 84 |
t.integer "user_id" |
81 | 85 |
t.integer "agent_id" |
82 |
- t.decimal "lat", precision: 15, scale: 10 |
|
83 |
- t.decimal "lng", precision: 15, scale: 10 |
|
84 |
- t.text "payload", limit: 2147483647 |
|
85 |
- t.datetime "created_at", null: false |
|
86 |
- t.datetime "updated_at", null: false |
|
86 |
+ t.decimal "lat", precision: 15, scale: 10 |
|
87 |
+ t.decimal "lng", precision: 15, scale: 10 |
|
88 |
+ t.text "payload" |
|
89 |
+ t.datetime "created_at" |
|
90 |
+ t.datetime "updated_at" |
|
87 | 91 |
t.datetime "expires_at" |
88 | 92 |
end |
89 | 93 |
|
@@ -94,8 +98,8 @@ ActiveRecord::Schema.define(version: 20140822085519) do |
||
94 | 98 |
create_table "links", force: true do |t| |
95 | 99 |
t.integer "source_id" |
96 | 100 |
t.integer "receiver_id" |
97 |
- t.datetime "created_at", null: false |
|
98 |
- t.datetime "updated_at", null: false |
|
101 |
+ t.datetime "created_at" |
|
102 |
+ t.datetime "updated_at" |
|
99 | 103 |
t.integer "event_id_at_creation", default: 0, null: false |
100 | 104 |
end |
101 | 105 |
|
@@ -113,24 +117,45 @@ ActiveRecord::Schema.define(version: 20140822085519) do |
||
113 | 117 |
add_index "scenario_memberships", ["scenario_id"], name: "index_scenario_memberships_on_scenario_id", using: :btree |
114 | 118 |
|
115 | 119 |
create_table "scenarios", force: true do |t| |
116 |
- t.string "name", null: false |
|
117 |
- t.integer "user_id", null: false |
|
120 |
+ t.string "name", null: false |
|
121 |
+ t.integer "user_id", null: false |
|
118 | 122 |
t.datetime "created_at" |
119 | 123 |
t.datetime "updated_at" |
120 | 124 |
t.text "description" |
121 |
- t.boolean "public", default: false, null: false |
|
122 |
- t.string "guid", null: false |
|
125 |
+ t.boolean "public", default: false, null: false |
|
126 |
+ t.string "guid", null: false |
|
123 | 127 |
t.string "source_url" |
128 |
+ t.string "tag_bg_color" |
|
129 |
+ t.string "tag_fg_color" |
|
124 | 130 |
end |
125 | 131 |
|
126 | 132 |
add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree |
127 | 133 |
|
134 |
+ create_table "services", force: true do |t| |
|
135 |
+ t.integer "user_id", null: false |
|
136 |
+ t.string "provider", null: false |
|
137 |
+ t.string "name", null: false |
|
138 |
+ t.text "token", null: false |
|
139 |
+ t.text "secret" |
|
140 |
+ t.text "refresh_token" |
|
141 |
+ t.datetime "expires_at" |
|
142 |
+ t.boolean "global", default: false |
|
143 |
+ t.text "options" |
|
144 |
+ t.datetime "created_at" |
|
145 |
+ t.datetime "updated_at" |
|
146 |
+ t.string "uid" |
|
147 |
+ end |
|
148 |
+ |
|
149 |
+ add_index "services", ["provider"], name: "index_services_on_provider", using: :btree |
|
150 |
+ add_index "services", ["uid"], name: "index_services_on_uid", using: :btree |
|
151 |
+ add_index "services", ["user_id", "global"], name: "index_services_on_user_id_and_global", using: :btree |
|
152 |
+ |
|
128 | 153 |
create_table "user_credentials", force: true do |t| |
129 | 154 |
t.integer "user_id", null: false |
130 | 155 |
t.string "credential_name", null: false |
131 | 156 |
t.text "credential_value", null: false |
132 |
- t.datetime "created_at", null: false |
|
133 |
- t.datetime "updated_at", null: false |
|
157 |
+ t.datetime "created_at" |
|
158 |
+ t.datetime "updated_at" |
|
134 | 159 |
t.string "mode", default: "text", null: false |
135 | 160 |
end |
136 | 161 |
|
@@ -147,8 +172,8 @@ ActiveRecord::Schema.define(version: 20140822085519) do |
||
147 | 172 |
t.datetime "last_sign_in_at" |
148 | 173 |
t.string "current_sign_in_ip" |
149 | 174 |
t.string "last_sign_in_ip" |
150 |
- t.datetime "created_at", null: false |
|
151 |
- t.datetime "updated_at", null: false |
|
175 |
+ t.datetime "created_at" |
|
176 |
+ t.datetime "updated_at" |
|
152 | 177 |
t.boolean "admin", default: false, null: false |
153 | 178 |
t.integer "failed_attempts", default: 0 |
154 | 179 |
t.string "unlock_token" |
@@ -16,6 +16,8 @@ class AgentsExporter |
||
16 | 16 |
:description => options[:description].presence || 'No description provided', |
17 | 17 |
:source_url => options[:source_url], |
18 | 18 |
:guid => options[:guid], |
19 |
+ :tag_fg_color => options[:tag_fg_color], |
|
20 |
+ :tag_bg_color => options[:tag_bg_color], |
|
19 | 21 |
:exported_at => Time.now.utc.iso8601, |
20 | 22 |
:agents => agents.map { |agent| agent_as_json(agent) }, |
21 | 23 |
:links => links |
@@ -51,4 +53,4 @@ class AgentsExporter |
||
51 | 53 |
options[:propagate_immediately] = agent.propagate_immediately if agent.can_receive_events? |
52 | 54 |
end |
53 | 55 |
end |
54 |
-end |
|
56 |
+end |
@@ -0,0 +1,35 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe LiquidInterpolatable::Filters do |
|
4 |
+ before do |
|
5 |
+ @filter = Class.new do |
|
6 |
+ include LiquidInterpolatable::Filters |
|
7 |
+ end.new |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ describe 'uri_escape' do |
|
11 |
+ it 'should escape a string for use in URI' do |
|
12 |
+ @filter.uri_escape('abc:/?=').should == 'abc%3A%2F%3F%3D' |
|
13 |
+ end |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ describe 'validations' do |
|
17 |
+ class Agents::InterpolatableAgent < Agent |
|
18 |
+ include LiquidInterpolatable |
|
19 |
+ |
|
20 |
+ def check |
|
21 |
+ create_event :payload => {} |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ def validate_options |
|
25 |
+ interpolated['foo'] |
|
26 |
+ end |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "should finish without raising an exception" do |
|
30 |
+ agent = Agents::InterpolatableAgent.new(name: "test", options: { 'foo' => '{{bar}' }) |
|
31 |
+ agent.valid?.should == false |
|
32 |
+ agent.errors[:options].first.should =~ /not properly terminated/ |
|
33 |
+ end |
|
34 |
+ end |
|
35 |
+end |
@@ -50,6 +50,8 @@ describe ScenariosController do |
||
50 | 50 |
assigns(:exporter).options[:description].should == scenarios(:bob_weather).description |
51 | 51 |
assigns(:exporter).options[:agents].should == scenarios(:bob_weather).agents |
52 | 52 |
assigns(:exporter).options[:guid].should == scenarios(:bob_weather).guid |
53 |
+ assigns(:exporter).options[:tag_fg_color].should == scenarios(:bob_weather).tag_fg_color |
|
54 |
+ assigns(:exporter).options[:tag_bg_color].should == scenarios(:bob_weather).tag_bg_color |
|
53 | 55 |
assigns(:exporter).options[:source_url].should be_falsey |
54 | 56 |
response.headers['Content-Disposition'].should == 'attachment; filename="bob-s-weather-alert-scenario.json"' |
55 | 57 |
response.headers['Content-Type'].should == 'application/json; charset=utf-8' |
@@ -0,0 +1,58 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ServicesController do |
|
4 |
+ before do |
|
5 |
+ sign_in users(:bob) |
|
6 |
+ OmniAuth.config.test_mode = true |
|
7 |
+ request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json'))) |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ describe "GET index" do |
|
11 |
+ it "only returns sevices of the current user" do |
|
12 |
+ get :index |
|
13 |
+ assigns(:services).all? {|i| i.user.should == users(:bob) }.should == true |
|
14 |
+ end |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ describe "POST toggle_availability" do |
|
18 |
+ it "should work for service of the user" do |
|
19 |
+ post :toggle_availability, :id => services(:generic).to_param |
|
20 |
+ assigns(:service).should eq(services(:generic)) |
|
21 |
+ redirect_to(services_path) |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ it "should not work for a service of another user" do |
|
25 |
+ lambda { |
|
26 |
+ post :toggle_availability, :id => services(:global).to_param |
|
27 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
28 |
+ end |
|
29 |
+ end |
|
30 |
+ |
|
31 |
+ describe "DELETE destroy" do |
|
32 |
+ it "destroys only services owned by the current user" do |
|
33 |
+ expect { |
|
34 |
+ delete :destroy, :id => services(:generic).to_param |
|
35 |
+ }.to change(Service, :count).by(-1) |
|
36 |
+ |
|
37 |
+ lambda { |
|
38 |
+ delete :destroy, :id => services(:global).to_param |
|
39 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
40 |
+ end |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ describe "accepting a callback url" do |
|
44 |
+ it "should update the user's credentials" do |
|
45 |
+ expect { |
|
46 |
+ get :callback, provider: 'twitter' |
|
47 |
+ }.to change { users(:bob).services.count }.by(1) |
|
48 |
+ end |
|
49 |
+ |
|
50 |
+ it "should work with an unknown provider (for now)" do |
|
51 |
+ request.env["omniauth.auth"]['provider'] = 'unknown' |
|
52 |
+ expect { |
|
53 |
+ get :callback, provider: 'unknown' |
|
54 |
+ }.to change { users(:bob).services.count }.by(1) |
|
55 |
+ users(:bob).services.first.provider.should == 'unknown' |
|
56 |
+ end |
|
57 |
+ end |
|
58 |
+end |
@@ -0,0 +1,43 @@ |
||
1 |
+{ |
|
2 |
+ "provider": "37signals", |
|
3 |
+ "uid": 12345, |
|
4 |
+ "info": { |
|
5 |
+ "email": "basecamp@none.de", |
|
6 |
+ "first_name": "Dominik", |
|
7 |
+ "last_name": "Sander", |
|
8 |
+ "name": "Dominik Sander" |
|
9 |
+ }, |
|
10 |
+ "credentials": { |
|
11 |
+ "token": "abcde", |
|
12 |
+ "refresh_token": "fghrefresh", |
|
13 |
+ "expires_at": 1401554352, |
|
14 |
+ "expires": true |
|
15 |
+ }, |
|
16 |
+ "extra": { |
|
17 |
+ "accounts": [ |
|
18 |
+ { |
|
19 |
+ "product": "bcx", |
|
20 |
+ "name": "Dominik Sander's Basecamp", |
|
21 |
+ "id": 12345, |
|
22 |
+ "href": "https://basecamp.com/12345/api/v1" |
|
23 |
+ } |
|
24 |
+ ], |
|
25 |
+ "raw_info": { |
|
26 |
+ "expires_at": "2014-05-31T16:39:12Z", |
|
27 |
+ "identity": { |
|
28 |
+ "first_name": "Dominik", |
|
29 |
+ "last_name": "Sander", |
|
30 |
+ "email_address": "basecamp@none.de", |
|
31 |
+ "id": 12345 |
|
32 |
+ }, |
|
33 |
+ "accounts": [ |
|
34 |
+ { |
|
35 |
+ "product": "bcx", |
|
36 |
+ "name": "Dominik Sander's Basecamp", |
|
37 |
+ "id": 12345, |
|
38 |
+ "href": "https://basecamp.com/12345/api/v1" |
|
39 |
+ } |
|
40 |
+ ] |
|
41 |
+ } |
|
42 |
+ } |
|
43 |
+} |
@@ -0,0 +1,52 @@ |
||
1 |
+{ |
|
2 |
+ "provider": "github", |
|
3 |
+ "uid": "12345", |
|
4 |
+ "info": { |
|
5 |
+ "nickname": "dsander", |
|
6 |
+ "email": null, |
|
7 |
+ "name": "Dominik Sander", |
|
8 |
+ "image": "https://avatars.githubusercontent.com/u/12345?", |
|
9 |
+ "urls": { |
|
10 |
+ "GitHub": "https://github.com/dsander", |
|
11 |
+ "Blog": "http://www.dsander.de" |
|
12 |
+ } |
|
13 |
+ }, |
|
14 |
+ "credentials": { |
|
15 |
+ "token": "agithubtoken", |
|
16 |
+ "expires": false |
|
17 |
+ }, |
|
18 |
+ "extra": { |
|
19 |
+ "raw_info": { |
|
20 |
+ "login": "dsander", |
|
21 |
+ "id": 12345, |
|
22 |
+ "avatar_url": "https://avatars.githubusercontent.com/u/12345?", |
|
23 |
+ "gravatar_id": "fsdfsdf", |
|
24 |
+ "url": "https://api.github.com/users/dsander", |
|
25 |
+ "html_url": "https://github.com/dsander", |
|
26 |
+ "followers_url": "https://api.github.com/users/dsander/followers", |
|
27 |
+ "following_url": "https://api.github.com/users/dsander/following{/other_user}", |
|
28 |
+ "gists_url": "https://api.github.com/users/dsander/gists{/gist_id}", |
|
29 |
+ "starred_url": "https://api.github.com/users/dsander/starred{/owner}{/repo}", |
|
30 |
+ "subscriptions_url": "https://api.github.com/users/dsander/subscriptions", |
|
31 |
+ "organizations_url": "https://api.github.com/users/dsander/orgs", |
|
32 |
+ "repos_url": "https://api.github.com/users/dsander/repos", |
|
33 |
+ "events_url": "https://api.github.com/users/dsander/events{/privacy}", |
|
34 |
+ "received_events_url": "https://api.github.com/users/dsander/received_events", |
|
35 |
+ "type": "User", |
|
36 |
+ "site_admin": false, |
|
37 |
+ "name": "Dominik Sander", |
|
38 |
+ "company": null, |
|
39 |
+ "blog": "http://www.url.de", |
|
40 |
+ "location": null, |
|
41 |
+ "email": null, |
|
42 |
+ "hireable": false, |
|
43 |
+ "bio": null, |
|
44 |
+ "public_repos": 29, |
|
45 |
+ "public_gists": 2, |
|
46 |
+ "followers": 21, |
|
47 |
+ "following": 9, |
|
48 |
+ "created_at": "2008-08-17T18:17:50Z", |
|
49 |
+ "updated_at": "2014-05-19T09:30:08Z" |
|
50 |
+ } |
|
51 |
+ } |
|
52 |
+} |
@@ -0,0 +1,66 @@ |
||
1 |
+{ |
|
2 |
+ "provider": "twitter", |
|
3 |
+ "uid": "123456", |
|
4 |
+ "info": { |
|
5 |
+ "nickname": "johnqpublic", |
|
6 |
+ "name": "John Q Public", |
|
7 |
+ "location": "Anytown, USA", |
|
8 |
+ "image": "http://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png", |
|
9 |
+ "description": "a very normal guy.", |
|
10 |
+ "urls": { |
|
11 |
+ "Website": null, |
|
12 |
+ "Twitter": "https://twitter.com/johnqpublic" |
|
13 |
+ } |
|
14 |
+ }, |
|
15 |
+ "credentials": { |
|
16 |
+ "token": "a1b2c3d4...", |
|
17 |
+ "secret": "abcdef1234" |
|
18 |
+ }, |
|
19 |
+ "extra": { |
|
20 |
+ "access_token": "", |
|
21 |
+ "raw_info": { |
|
22 |
+ "name": "John Q Public", |
|
23 |
+ "listed_count": 0, |
|
24 |
+ "profile_sidebar_border_color": "181A1E", |
|
25 |
+ "url": null, |
|
26 |
+ "lang": "en", |
|
27 |
+ "statuses_count": 129, |
|
28 |
+ "profile_image_url": "http://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png", |
|
29 |
+ "profile_background_image_url_https": "https://twimg0-a.akamaihd.net/profile_background_images/229171796/pattern_036.gif", |
|
30 |
+ "location": "Anytown, USA", |
|
31 |
+ "time_zone": "Chicago", |
|
32 |
+ "follow_request_sent": false, |
|
33 |
+ "id": 123456, |
|
34 |
+ "profile_background_tile": true, |
|
35 |
+ "profile_sidebar_fill_color": "666666", |
|
36 |
+ "followers_count": 1, |
|
37 |
+ "default_profile_image": false, |
|
38 |
+ "screen_name": "", |
|
39 |
+ "following": false, |
|
40 |
+ "utc_offset": -3600, |
|
41 |
+ "verified": false, |
|
42 |
+ "favourites_count": 0, |
|
43 |
+ "profile_background_color": "1A1B1F", |
|
44 |
+ "is_translator": false, |
|
45 |
+ "friends_count": 1, |
|
46 |
+ "notifications": false, |
|
47 |
+ "geo_enabled": true, |
|
48 |
+ "profile_background_image_url": "http://twimg0-a.akamaihd.net/profile_background_images/229171796/pattern_036.gif", |
|
49 |
+ "protected": false, |
|
50 |
+ "description": "a very normal guy.", |
|
51 |
+ "profile_link_color": "2FC2EF", |
|
52 |
+ "created_at": "Thu Jul 4 00:00:00 +0000 2013", |
|
53 |
+ "id_str": "123456", |
|
54 |
+ "profile_image_url_https": "https://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png", |
|
55 |
+ "default_profile": false, |
|
56 |
+ "profile_use_background_image": false, |
|
57 |
+ "entities": { |
|
58 |
+ "description": { |
|
59 |
+ "urls": [] |
|
60 |
+ } |
|
61 |
+ }, |
|
62 |
+ "profile_text_color": "666666", |
|
63 |
+ "contributors_enabled": false |
|
64 |
+ } |
|
65 |
+ } |
|
66 |
+} |
@@ -0,0 +1,5 @@ |
||
1 |
+APP_SECRET_TOKEN=notarealappsecrettoken |
|
2 |
+TWITTER_OAUTH_KEY=twitteroauthkey |
|
3 |
+TWITTER_OAUTH_SECRET=twitteroauthsecret |
|
4 |
+THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY |
|
5 |
+THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET |
@@ -109,3 +109,15 @@ bob_manual_event_agent: |
||
109 | 109 |
user: bob |
110 | 110 |
name: "Bob's event testing agent" |
111 | 111 |
guid: <%= SecureRandom.hex %> |
112 |
+ |
|
113 |
+bob_basecamp_agent: |
|
114 |
+ type: Agents::BasecampAgent |
|
115 |
+ user: bob |
|
116 |
+ service: generic |
|
117 |
+ guid: <%= SecureRandom.hex %> |
|
118 |
+ |
|
119 |
+jane_basecamp_agent: |
|
120 |
+ type: Agents::BasecampAgent |
|
121 |
+ user: jane |
|
122 |
+ service: generic |
|
123 |
+ guid: <%= SecureRandom.hex %> |
@@ -0,0 +1,17 @@ |
||
1 |
+generic: |
|
2 |
+ token: 1234token |
|
3 |
+ secret: 56789secret |
|
4 |
+ refresh_token: refresh12345 |
|
5 |
+ provider: testprovider |
|
6 |
+ name: test |
|
7 |
+ expires_at: <%= Time.parse("2015-01-01 00:00:00") %> |
|
8 |
+ options: <%= { user_id: 12345 }.to_yaml.inspect %> |
|
9 |
+ user: bob |
|
10 |
+global: |
|
11 |
+ token: 1234token |
|
12 |
+ provider: testprovider |
|
13 |
+ name: test |
|
14 |
+ expires_at: <%= Time.parse("2015-01-01 00:00:00") %> |
|
15 |
+ options: <%= { user_id: 12345 }.to_yaml.inspect %> |
|
16 |
+ user: jane |
|
17 |
+ global: true |
@@ -0,0 +1,30 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioHelper do |
|
4 |
+ let(:scenario) { users(:bob).scenarios.build(name: 'Scene', tag_fg_color: '#AAAAAA', tag_bg_color: '#000000') } |
|
5 |
+ |
|
6 |
+ describe '#style_colors' do |
|
7 |
+ it 'returns a css style-formated version of the scenario foreground and background colors' do |
|
8 |
+ style_colors(scenario).should == "color:#AAAAAA;background-color:#000000" |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ it 'defauls foreground and background colors' do |
|
12 |
+ scenario.tag_fg_color = nil |
|
13 |
+ scenario.tag_bg_color = nil |
|
14 |
+ style_colors(scenario).should == "color:#FFFFFF;background-color:#5BC0DE" |
|
15 |
+ end |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ describe '#scenario_label' do |
|
19 |
+ it 'creates a scenario label with the scenario name' do |
|
20 |
+ scenario_label(scenario).should == |
|
21 |
+ '<span class="label scenario" style="color:#AAAAAA;background-color:#000000">Scene</span>' |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ it 'creates a scenario label with the given text' do |
|
25 |
+ scenario_label(scenario, 'Other').should == |
|
26 |
+ '<span class="label scenario" style="color:#AAAAAA;background-color:#000000">Other</span>' |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+end |
@@ -7,9 +7,13 @@ describe AgentsExporter do |
||
7 | 7 |
let(:name) { "My set of Agents" } |
8 | 8 |
let(:description) { "These Agents work together nicely!" } |
9 | 9 |
let(:guid) { "some-guid" } |
10 |
+ let(:tag_fg_color) { "#ffffff" } |
|
11 |
+ let(:tag_bg_color) { "#000000" } |
|
10 | 12 |
let(:source_url) { "http://yourhuginn.com/scenarios/2/export.json" } |
11 | 13 |
let(:agent_list) { [agents(:jane_weather_agent), agents(:jane_rain_notifier_agent)] } |
12 |
- let(:exporter) { AgentsExporter.new(:agents => agent_list, :name => name, :description => description, :source_url => source_url, :guid => guid) } |
|
14 |
+ let(:exporter) { AgentsExporter.new( |
|
15 |
+ :agents => agent_list, :name => name, :description => description, :source_url => source_url, |
|
16 |
+ :guid => guid, :tag_fg_color => tag_fg_color, :tag_bg_color => tag_bg_color) } |
|
13 | 17 |
|
14 | 18 |
it "outputs a structure containing name, description, the date, all agents & their links" do |
15 | 19 |
data = exporter.as_json |
@@ -17,6 +21,8 @@ describe AgentsExporter do |
||
17 | 21 |
data[:description].should == description |
18 | 22 |
data[:source_url].should == source_url |
19 | 23 |
data[:guid].should == guid |
24 |
+ data[:tag_fg_color].should == tag_fg_color |
|
25 |
+ data[:tag_bg_color].should == tag_bg_color |
|
20 | 26 |
Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) |
21 | 27 |
data[:links].should == [{ :source => 0, :receiver => 1 }] |
22 | 28 |
data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } |
@@ -58,4 +64,4 @@ describe AgentsExporter do |
||
58 | 64 |
AgentsExporter.new(:name => ",,").filename.should == "exported-agents.json" |
59 | 65 |
end |
60 | 66 |
end |
61 |
-end |
|
67 |
+end |
@@ -1,17 +1,16 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/oauthable' |
|
2 | 3 |
|
3 | 4 |
describe Agents::BasecampAgent do |
5 |
+ it_behaves_like Oauthable |
|
6 |
+ |
|
4 | 7 |
before(:each) do |
5 | 8 |
stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
6 |
- stub_request(:get, /Z$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
7 |
- @valid_params = { |
|
8 |
- :username => "user", |
|
9 |
- :password => "pass", |
|
10 |
- :user_id => 12345, |
|
11 |
- :project_id => 6789, |
|
12 |
- } |
|
9 |
+ stub_request(:get, /02:00$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
10 |
+ @valid_params = { :project_id => 6789 } |
|
13 | 11 |
|
14 | 12 |
@checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params) |
13 |
+ @checker.service = services(:generic) |
|
15 | 14 |
@checker.user = users(:jane) |
16 | 15 |
@checker.save! |
17 | 16 |
end |
@@ -21,21 +20,6 @@ describe Agents::BasecampAgent do |
||
21 | 20 |
@checker.should be_valid |
22 | 21 |
end |
23 | 22 |
|
24 |
- it "should require the basecamp username" do |
|
25 |
- @checker.options['username'] = nil |
|
26 |
- @checker.should_not be_valid |
|
27 |
- end |
|
28 |
- |
|
29 |
- it "should require the basecamp password" do |
|
30 |
- @checker.options['password'] = nil |
|
31 |
- @checker.should_not be_valid |
|
32 |
- end |
|
33 |
- |
|
34 |
- it "should require the basecamp user_id" do |
|
35 |
- @checker.options['user_id'] = nil |
|
36 |
- @checker.should_not be_valid |
|
37 |
- end |
|
38 |
- |
|
39 | 23 |
it "should require the basecamp project_id" do |
40 | 24 |
@checker.options['project_id'] = nil |
41 | 25 |
@checker.should_not be_valid |
@@ -45,7 +29,7 @@ describe Agents::BasecampAgent do |
||
45 | 29 |
|
46 | 30 |
describe "helpers" do |
47 | 31 |
it "should generate a correct request options hash" do |
48 |
- @checker.send(:request_options).should == {:basic_auth=>{:username=>"user", :password=>"pass"}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}} |
|
32 |
+ @checker.send(:request_options).should == {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => 'Bearer "1234token"'}} |
|
49 | 33 |
end |
50 | 34 |
|
51 | 35 |
it "should generate the currect request url" do |
@@ -59,7 +43,7 @@ describe Agents::BasecampAgent do |
||
59 | 43 |
|
60 | 44 |
it "should provide the since attribute after the first run" do |
61 | 45 |
time = (Time.now-1.minute).iso8601 |
62 |
- @checker.memory[:last_run] = time |
|
46 |
+ @checker.memory[:last_event] = time |
|
63 | 47 |
@checker.save |
64 | 48 |
@checker.reload.send(:query_parameters).should == {:query => {:since => time}} |
65 | 49 |
end |
@@ -67,9 +51,10 @@ describe Agents::BasecampAgent do |
||
67 | 51 |
describe "#check" do |
68 | 52 |
it "should not emit events on its first run" do |
69 | 53 |
expect { @checker.check }.to change { Event.count }.by(0) |
54 |
+ expect(@checker.memory[:last_event]).to eq '2014-04-17T10:25:31.000+02:00' |
|
70 | 55 |
end |
71 | 56 |
it "should check that initial run creates an event" do |
72 |
- @checker.last_check_at = Time.now - 1.minute |
|
57 |
+ @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00' |
|
73 | 58 |
expect { @checker.check }.to change { Event.count }.by(1) |
74 | 59 |
end |
75 | 60 |
end |
@@ -77,7 +62,7 @@ describe Agents::BasecampAgent do |
||
77 | 62 |
describe "#working?" do |
78 | 63 |
it "it is working when at least one event was emited" do |
79 | 64 |
@checker.should_not be_working |
80 |
- @checker.last_check_at = Time.now - 1.minute |
|
65 |
+ @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00' |
|
81 | 66 |
@checker.check |
82 | 67 |
@checker.reload.should be_working |
83 | 68 |
end |
@@ -13,6 +13,7 @@ describe Agents::TwitterPublishAgent do |
||
13 | 13 |
} |
14 | 14 |
|
15 | 15 |
@checker = Agents::TwitterPublishAgent.new(:name => "HuginnBot", :options => @opts) |
16 |
+ @checker.service = services(:generic) |
|
16 | 17 |
@checker.user = users(:bob) |
17 | 18 |
@checker.save! |
18 | 19 |
|
@@ -13,6 +13,7 @@ describe Agents::TwitterStreamAgent do |
||
13 | 13 |
} |
14 | 14 |
|
15 | 15 |
@agent = Agents::TwitterStreamAgent.new(:name => "HuginnBot", :options => @opts) |
16 |
+ @agent.service = services(:generic) |
|
16 | 17 |
@agent.user = users(:bob) |
17 | 18 |
@agent.save! |
18 | 19 |
end |
@@ -16,6 +16,7 @@ describe Agents::TwitterUserAgent do |
||
16 | 16 |
} |
17 | 17 |
|
18 | 18 |
@checker = Agents::TwitterUserAgent.new(:name => "tectonic", :options => @opts) |
19 |
+ @checker.service = services(:generic) |
|
19 | 20 |
@checker.user = users(:bob) |
20 | 21 |
@checker.save! |
21 | 22 |
end |
@@ -31,6 +32,7 @@ describe Agents::TwitterUserAgent do |
||
31 | 32 |
opts = @opts.merge({ :starting_at => "Jan 01 00:00:01 +0000 2999", }) |
32 | 33 |
|
33 | 34 |
checker = Agents::TwitterUserAgent.new(:name => "tectonic", :options => opts) |
35 |
+ checker.service = services(:generic) |
|
34 | 36 |
checker.user = users(:bob) |
35 | 37 |
checker.save! |
36 | 38 |
|
@@ -453,16 +453,32 @@ fire: hot |
||
453 | 453 |
end |
454 | 454 |
|
455 | 455 |
describe "#receive" do |
456 |
- it "should scrape from the url element in incoming event payload" do |
|
456 |
+ before do |
|
457 | 457 |
@event = Event.new |
458 | 458 |
@event.agent = agents(:bob_rain_notifier_agent) |
459 | 459 |
@event.payload = { 'url' => "http://xkcd.com" } |
460 |
+ end |
|
460 | 461 |
|
462 |
+ it "should scrape from the url element in incoming event payload" do |
|
461 | 463 |
lambda { |
462 | 464 |
@checker.options = @valid_options |
463 | 465 |
@checker.receive([@event]) |
464 | 466 |
}.should change { Event.count }.by(1) |
465 | 467 |
end |
468 |
+ |
|
469 |
+ it "should interpolate values from incoming event payload" do |
|
470 |
+ @event.payload['title'] = 'XKCD' |
|
471 |
+ |
|
472 |
+ lambda { |
|
473 |
+ @valid_options['extract']['site_title'] = { |
|
474 |
+ 'css' => "#comic img", 'value' => "'{{title}}'" |
|
475 |
+ } |
|
476 |
+ @checker.options = @valid_options |
|
477 |
+ @checker.receive([@event]) |
|
478 |
+ }.should change { Event.count }.by(1) |
|
479 |
+ |
|
480 |
+ Event.last.payload['site_title'].should == 'XKCD' |
|
481 |
+ end |
|
466 | 482 |
end |
467 | 483 |
end |
468 | 484 |
|
@@ -0,0 +1,29 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+module Agents |
|
4 |
+ class OauthableTestAgent < Agent |
|
5 |
+ include Oauthable |
|
6 |
+ end |
|
7 |
+end |
|
8 |
+ |
|
9 |
+shared_examples_for Oauthable do |
|
10 |
+ before(:each) do |
|
11 |
+ @agent = described_class.new(:name => "somename") |
|
12 |
+ @agent.user = users(:jane) |
|
13 |
+ end |
|
14 |
+ |
|
15 |
+ it "should be oauthable" do |
|
16 |
+ @agent.oauthable?.should == true |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ describe "valid_services_for" do |
|
20 |
+ it "should return all available services without specifying valid_oauth_providers" do |
|
21 |
+ @agent = Agents::OauthableTestAgent.new |
|
22 |
+ @agent.valid_services_for(users(:bob)).collect(&:id).sort.should == [services(:generic), services(:global)].collect(&:id).sort |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ it "should filter the services based on the agent defaults" do |
|
26 |
+ @agent.valid_services_for(users(:bob)).to_a.should == Service.where(provider: @agent.valid_oauth_providers) |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+end |
@@ -102,6 +102,15 @@ describe EventDrop do |
||
102 | 102 |
interpolate(t, @event).should eq('some title: http://some.site.example.org/') |
103 | 103 |
end |
104 | 104 |
|
105 |
+ it 'should use created_at from the payload if it exists' do |
|
106 |
+ created_at = @event.created_at - 86400 |
|
107 |
+ # Avoid timezone issue by using %s |
|
108 |
+ @event.payload['created_at'] = created_at.strftime("%s") |
|
109 |
+ @event.save! |
|
110 |
+ t = '{{created_at | date:"%s" }}' |
|
111 |
+ interpolate(t, @event).should eq(created_at.strftime("%s")) |
|
112 |
+ end |
|
113 |
+ |
|
105 | 114 |
it 'should be iteratable' do |
106 | 115 |
# to_liquid returns self |
107 | 116 |
t = "{% for pair in to_liquid %}{{pair | join:':' }}\n{% endfor %}" |
@@ -3,6 +3,8 @@ require 'spec_helper' |
||
3 | 3 |
describe ScenarioImport do |
4 | 4 |
let(:user) { users(:bob) } |
5 | 5 |
let(:guid) { "somescenarioguid" } |
6 |
+ let(:tag_fg_color) { "#ffffff" } |
|
7 |
+ let(:tag_bg_color) { "#000000" } |
|
6 | 8 |
let(:description) { "This is a cool Huginn Scenario that does something useful!" } |
7 | 9 |
let(:name) { "A useful Scenario" } |
8 | 10 |
let(:source_url) { "http://example.com/scenarios/2/export.json" } |
@@ -45,11 +47,25 @@ describe ScenarioImport do |
||
45 | 47 |
:options => trigger_agent_options |
46 | 48 |
} |
47 | 49 |
end |
50 |
+ let(:valid_parsed_basecamp_agent_data) do |
|
51 |
+ { |
|
52 |
+ :type => "Agents::BasecampAgent", |
|
53 |
+ :name => "Basecamp test", |
|
54 |
+ :schedule => "every_2m", |
|
55 |
+ :keep_events_for => 0, |
|
56 |
+ :propagate_immediately => true, |
|
57 |
+ :disabled => false, |
|
58 |
+ :guid => "a-basecamp-agent", |
|
59 |
+ :options => {project_id: 12345} |
|
60 |
+ } |
|
61 |
+ end |
|
48 | 62 |
let(:valid_parsed_data) do |
49 |
- { |
|
63 |
+ { |
|
50 | 64 |
:name => name, |
51 | 65 |
:description => description, |
52 | 66 |
:guid => guid, |
67 |
+ :tag_fg_color => tag_fg_color, |
|
68 |
+ :tag_bg_color => tag_bg_color, |
|
53 | 69 |
:source_url => source_url, |
54 | 70 |
:exported_at => 2.days.ago.utc.iso8601, |
55 | 71 |
:agents => [ |
@@ -142,7 +158,7 @@ describe ScenarioImport do |
||
142 | 158 |
end |
143 | 159 |
end |
144 | 160 |
end |
145 |
- |
|
161 |
+ |
|
146 | 162 |
describe "#dangerous?" do |
147 | 163 |
it "returns false on most Agents" do |
148 | 164 |
ScenarioImport.new(:data => valid_data).should_not be_dangerous |
@@ -171,6 +187,8 @@ describe ScenarioImport do |
||
171 | 187 |
scenario_import.scenario.name.should == name |
172 | 188 |
scenario_import.scenario.description.should == description |
173 | 189 |
scenario_import.scenario.guid.should == guid |
190 |
+ scenario_import.scenario.tag_fg_color.should == tag_fg_color |
|
191 |
+ scenario_import.scenario.tag_bg_color.should == tag_bg_color |
|
174 | 192 |
scenario_import.scenario.source_url.should == source_url |
175 | 193 |
scenario_import.scenario.public.should be_falsey |
176 | 194 |
end |
@@ -269,6 +287,8 @@ describe ScenarioImport do |
||
269 | 287 |
|
270 | 288 |
existing_scenario.reload |
271 | 289 |
existing_scenario.guid.should == guid |
290 |
+ existing_scenario.tag_fg_color.should == tag_fg_color |
|
291 |
+ existing_scenario.tag_bg_color.should == tag_bg_color |
|
272 | 292 |
existing_scenario.description.should == description |
273 | 293 |
existing_scenario.name.should == name |
274 | 294 |
existing_scenario.source_url.should == source_url |
@@ -407,5 +427,48 @@ describe ScenarioImport do |
||
407 | 427 |
end |
408 | 428 |
end |
409 | 429 |
end |
430 |
+ |
|
431 |
+ context "agents which require a service" do |
|
432 |
+ let(:valid_parsed_services) do |
|
433 |
+ data = valid_parsed_data |
|
434 |
+ data[:agents] = [valid_parsed_basecamp_agent_data, |
|
435 |
+ valid_parsed_trigger_agent_data] |
|
436 |
+ data |
|
437 |
+ end |
|
438 |
+ |
|
439 |
+ let(:valid_parsed_services_data) { valid_parsed_services.to_json } |
|
440 |
+ |
|
441 |
+ let(:services_scenario_import) { |
|
442 |
+ _import = ScenarioImport.new(:data => valid_parsed_services_data) |
|
443 |
+ _import.set_user users(:bob) |
|
444 |
+ _import |
|
445 |
+ } |
|
446 |
+ |
|
447 |
+ describe "#generate_diff" do |
|
448 |
+ it "should check if the agent requires a service" do |
|
449 |
+ agent_diffs = services_scenario_import.agent_diffs |
|
450 |
+ basecamp_agent_diff = agent_diffs[0] |
|
451 |
+ basecamp_agent_diff.requires_service?.should == true |
|
452 |
+ end |
|
453 |
+ |
|
454 |
+ it "should add an error when no service is selected" do |
|
455 |
+ services_scenario_import.import.should == false |
|
456 |
+ services_scenario_import.errors[:base].length.should == 1 |
|
457 |
+ end |
|
458 |
+ end |
|
459 |
+ |
|
460 |
+ describe "#import" do |
|
461 |
+ it "should import" do |
|
462 |
+ services_scenario_import.merges = { |
|
463 |
+ "0" => { |
|
464 |
+ "service_id" => "0", |
|
465 |
+ } |
|
466 |
+ } |
|
467 |
+ lambda { |
|
468 |
+ services_scenario_import.import.should == true |
|
469 |
+ }.should change { users(:bob).agents.count }.by(2) |
|
470 |
+ end |
|
471 |
+ end |
|
472 |
+ end |
|
410 | 473 |
end |
411 |
-end |
|
474 |
+end |
@@ -20,6 +20,30 @@ describe Scenario do |
||
20 | 20 |
new_instance.should_not be_valid |
21 | 21 |
end |
22 | 22 |
|
23 |
+ it "validates tag_fg_color is hex color" do |
|
24 |
+ new_instance.tag_fg_color = '#N07H3X' |
|
25 |
+ new_instance.should_not be_valid |
|
26 |
+ new_instance.tag_fg_color = '#BADA55' |
|
27 |
+ new_instance.should be_valid |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ it "allows nil tag_fg_color" do |
|
31 |
+ new_instance.tag_fg_color = nil |
|
32 |
+ new_instance.should be_valid |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ it "validates tag_bg_color is hex color" do |
|
36 |
+ new_instance.tag_bg_color = '#N07H3X' |
|
37 |
+ new_instance.should_not be_valid |
|
38 |
+ new_instance.tag_bg_color = '#BADA55' |
|
39 |
+ new_instance.should be_valid |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ it "allows nil tag_bg_color" do |
|
43 |
+ new_instance.tag_bg_color = nil |
|
44 |
+ new_instance.should be_valid |
|
45 |
+ end |
|
46 |
+ |
|
23 | 47 |
it "only allows Agents owned by user" do |
24 | 48 |
new_instance.agent_ids = [agents(:bob_website_agent).id] |
25 | 49 |
new_instance.should be_valid |
@@ -0,0 +1,129 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Service do |
|
4 |
+ before(:each) do |
|
5 |
+ @user = users(:bob) |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ describe "#toggle_availability!" do |
|
9 |
+ it "should toggle the global flag" do |
|
10 |
+ @service = services(:generic) |
|
11 |
+ @service.global.should == false |
|
12 |
+ @service.toggle_availability! |
|
13 |
+ @service.global.should == true |
|
14 |
+ @service.toggle_availability! |
|
15 |
+ @service.global.should == false |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ it "disconnects agents and disables them if the previously global service is made private again", focus: true do |
|
19 |
+ agent = agents(:bob_basecamp_agent) |
|
20 |
+ jane_agent = agents(:jane_basecamp_agent) |
|
21 |
+ |
|
22 |
+ service = agent.service |
|
23 |
+ service.toggle_availability! |
|
24 |
+ service.agents.length.should == 2 |
|
25 |
+ |
|
26 |
+ service.toggle_availability! |
|
27 |
+ jane_agent.reload |
|
28 |
+ jane_agent.service_id.should be_nil |
|
29 |
+ jane_agent.disabled.should be true |
|
30 |
+ |
|
31 |
+ service.reload |
|
32 |
+ service.agents.length.should == 1 |
|
33 |
+ end |
|
34 |
+ end |
|
35 |
+ |
|
36 |
+ it "disables all agents before beeing destroyed" do |
|
37 |
+ agent = agents(:bob_basecamp_agent) |
|
38 |
+ service = agent.service |
|
39 |
+ service.destroy |
|
40 |
+ agent.reload |
|
41 |
+ agent.service_id.should be_nil |
|
42 |
+ agent.disabled.should be true |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ describe "preparing for a request" do |
|
46 |
+ before(:each) do |
|
47 |
+ @service = services(:generic) |
|
48 |
+ end |
|
49 |
+ |
|
50 |
+ it "should not update the token if the token never expires" do |
|
51 |
+ @service.expires_at = nil |
|
52 |
+ @service.prepare_request.should == nil |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ it "should not update the token if the token is still valid" do |
|
56 |
+ @service.expires_at = Time.now + 1.hour |
|
57 |
+ @service.prepare_request.should == nil |
|
58 |
+ end |
|
59 |
+ |
|
60 |
+ it "should call refresh_token! if the token expired" do |
|
61 |
+ stub(@service).refresh_token! { @service } |
|
62 |
+ @service.expires_at = Time.now - 1.hour |
|
63 |
+ @service.prepare_request.should == @service |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+ |
|
67 |
+ describe "updating the access token" do |
|
68 |
+ before(:each) do |
|
69 |
+ @service = services(:generic) |
|
70 |
+ end |
|
71 |
+ |
|
72 |
+ it "should return the correct endpoint" do |
|
73 |
+ @service.provider = '37signals' |
|
74 |
+ @service.send(:endpoint).to_s.should == "https://launchpad.37signals.com/authorization/token" |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ it "should update the token" do |
|
78 |
+ stub_request(:post, "https://launchpad.37signals.com/authorization/token?client_id=TESTKEY&client_secret=TESTSECRET&refresh_token=refreshtokentest&type=refresh"). |
|
79 |
+ to_return(:status => 200, :body => '{"expires_in":1209600,"access_token": "NEWTOKEN"}', :headers => {}) |
|
80 |
+ @service.provider = '37signals' |
|
81 |
+ @service.refresh_token = 'refreshtokentest' |
|
82 |
+ @service.refresh_token! |
|
83 |
+ @service.token.should == 'NEWTOKEN' |
|
84 |
+ end |
|
85 |
+ end |
|
86 |
+ |
|
87 |
+ describe "creating services via omniauth" do |
|
88 |
+ it "should work with twitter services" do |
|
89 |
+ twitter = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json'))) |
|
90 |
+ expect { |
|
91 |
+ service = @user.services.initialize_or_update_via_omniauth(twitter) |
|
92 |
+ service.save! |
|
93 |
+ }.to change { @user.services.count }.by(1) |
|
94 |
+ service = @user.services.first |
|
95 |
+ service.name.should == 'johnqpublic' |
|
96 |
+ service.uid.should == '123456' |
|
97 |
+ service.provider.should == 'twitter' |
|
98 |
+ service.token.should == 'a1b2c3d4...' |
|
99 |
+ service.secret.should == 'abcdef1234' |
|
100 |
+ end |
|
101 |
+ it "should work with 37signals services" do |
|
102 |
+ signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/37signals.json'))) |
|
103 |
+ expect { |
|
104 |
+ service = @user.services.initialize_or_update_via_omniauth(signals) |
|
105 |
+ service.save! |
|
106 |
+ }.to change { @user.services.count }.by(1) |
|
107 |
+ service = @user.services.first |
|
108 |
+ service.provider.should == '37signals' |
|
109 |
+ service.name.should == 'Dominik Sander' |
|
110 |
+ service.token.should == 'abcde' |
|
111 |
+ service.uid.should == '12345' |
|
112 |
+ service.refresh_token.should == 'fghrefresh' |
|
113 |
+ service.options[:user_id].should == 12345 |
|
114 |
+ service.expires_at = Time.at(1401554352) |
|
115 |
+ end |
|
116 |
+ it "should work with github services" do |
|
117 |
+ signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/github.json'))) |
|
118 |
+ expect { |
|
119 |
+ service = @user.services.initialize_or_update_via_omniauth(signals) |
|
120 |
+ service.save! |
|
121 |
+ }.to change { @user.services.count }.by(1) |
|
122 |
+ service = @user.services.first |
|
123 |
+ service.provider.should == 'github' |
|
124 |
+ service.name.should == 'dsander' |
|
125 |
+ service.uid.should == '12345' |
|
126 |
+ service.token.should == 'agithubtoken' |
|
127 |
+ end |
|
128 |
+ end |
|
129 |
+end |
@@ -1,4 +1,3 @@ |
||
1 |
-# This file is copied to spec/ when you run 'rails generate rspec:install' |
|
2 | 1 |
ENV["RAILS_ENV"] ||= 'test' |
3 | 2 |
|
4 | 3 |
if ENV['COVERAGE'] |
@@ -9,6 +8,10 @@ else |
||
9 | 8 |
Coveralls.wear!('rails') |
10 | 9 |
end |
11 | 10 |
|
11 |
+# Required ENV variables that are normally set in .env are setup here for the test environment. |
|
12 |
+require 'dotenv' |
|
13 |
+Dotenv.overload File.join(File.dirname(__FILE__), "env.test") |
|
14 |
+ |
|
12 | 15 |
require File.expand_path("../../config/environment", __FILE__) |
13 | 16 |
require 'rspec/rails' |
14 | 17 |
require 'rspec/autorun' |
@@ -19,7 +22,9 @@ WebMock.disable_net_connect! |
||
19 | 22 |
|
20 | 23 |
# Requires supporting ruby files with custom matchers and macros, etc, |
21 | 24 |
# in spec/support/ and its subdirectories. |
22 |
-Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} |
|
25 |
+Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } |
|
26 |
+ |
|
27 |
+ActiveRecord::Migration.maintain_test_schema! |
|
23 | 28 |
|
24 | 29 |
RSpec.configure do |config| |
25 | 30 |
config.mock_with :rr |